home *** CD-ROM | disk | FTP | other *** search
Wrap
Text File | 1997-02-18 | 170.4 KB | 4,681 lines | [ TEXT/CWIE]
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Apple Macintosh Developer Technical Support MultiFinder-Aware SoundApp Application SoundApp SoundApp.c - C Source Jim Reekes - Macintosh Developer Technical Support Copyright © 1989-1994 Apple Computer, Inc. All rights reserved. Versions: 1.03 January, 1990 1.04 Sept, 1990 1.2 August, 1994 translated to C Components: SoundApp.c January, 1990 MPW C source code SoundUnit.c January, 1990 MPW C source code SoundUnit.h January, 1990 MPW C source code SoundApp.r January, 1990 MPW Rez source code SoundAppSnds.r January, 1990 MPW Rez source code SoundApp.make January, 1990 MPW build script Formatting was done with FONT = Courier or Monaco, SIZE = 10, TABS = 4 Version comments 1.04: This was an update to support the Utilities Unit that MacDTS developed. Some of it was routines formally created for SoundApp, with additional ones coming from other sources. This helps to reduce the "destractive" code from the samples and makes them easier to maintain. 1.1: • This is the "new" SoundApp which adds some new features. • Some knowledge of the new Sound Manager is present in areas that were work-arounds for old Sound Manager bugs • Recording of sounds is now possible with the Sound Input Manager • The document window will keep the Record button hidden if the Sound Inpute Manager is not available for recording, otherwise it will expand the window size to show this button. • Supports copy and pasting of sound resources • Conversion to MPW 3.2 was established (with some amount of pain) • SoundApp now has its own documents • Supports launching by open documents from Finder • Added color icons for the System 7.0 desktop • Added the new Sound Manager error strings • DoErrorSound tests for safe beeps • Made Apple Legal happy by getting rid of the xxxxCmd and changed it to the freqDurationCmd. Also got rid of any other use of the work "xxxx" in a musical context 1.2: • Translated into C, some clean up along the way • Using universal headers • Gave up on the DTS utilities code, everything is self-contained Formatting was done with FONT = Courier, SIZE = 10, TABS = 4 many thanks to: Bo3b Johnson, Mark Bennett, Andy Shebanow, Keith Rollin, Chris Derossi, Pete Helme, Darin Adler, and my co-workers that sat near me while I was making lots of noise testing this application. To the reader, SoundApp.p is a sample application source file for demonstrating the Sound Manager. It requires the use of the SoundUnit to handle all of the sound routines. This portion of the source code handles the application’s management of memory, errors, user interface, etc.. The read me notes are here in the source code because people tend to not read them otherwise. SoundApp is a demonstration of the Sound Manager released in System 6.0.x. SoundApp is also a example of how to write a Macintosh application that performs good user interface and proper error handling. Believe it or not, but the Sound Manager portion was the easiest part for me to write. It was all the user interface and error handling that was the most difficult. One thing became very clear to me in the course of writing this application. The following axiom is one of the few great truths in the universe. If you write a Macintosh application without MacApp, you’re working too hard. Throughout the development of SoundApp I would run into typical situations that make programming a Macintosh too hard. When this happened I asked myself “what would MacApp do?” and that was followed by the thought “then why am I writing code that is already out there for me to use?” I started out intending on writing a very simple application that anyone could read, and understand the Sound Manager. I felt this meant not requiring the person to read Object Oriented Pascal. I accomplished part of my goal. People should be able to learn how to use the Sound Manager (in its present condition), but it didn’t turn out to be such a simple application as I had hoped for. I have put a large amount of comments into the code. This is something I’m really picky about. People do not comment their code enough. Each procedure has a paragraph that should explain what to expect that routine to do, and how it goes about doing it. There are some bigger issues that I will put into the release notes below. There are some things that make the Macintosh harder to develop for than it should be. Simple things should be simple. Too many things on the Mac that should be simple are not. Maybe someday these things will be fixed. • GetFontInfo requires a port set to the font in question. If I wanted to find the height of the System Font, I had to first set the current port to the WindowMgr. I could have used my own window, but what if I needed the font info before I had a window available? • The toolbox blows chunks when your heap gets “too low.” I believe this magical number is between 32k and zero. The odds of blowing chunks increase logarithmically as one approaches 0 free bytes. • The Dialog Manager is not a free lunch and in fact will cost you plenty. • There’s no safe way to determine how much memory opening a resource file will take away from your heap. • There’s no way to determine how safe it is opening a resource that could be shared by other applications, especially on a local volume. • The Resource Manager doesn’t always set ResErr. • The Sound Manager returns even less errors. • The List Manager returns no errors. • Writing a staggering routine for new windows encompasses a number of difficulties. How does one find the height of a window’s title region before the window is visible? Am I just a complainer? Do I have a bad attitude? Probably, but I’m just trying to point out the above areas make the Mac programming experience difficult. These are areas that get developers into trouble. These areas need a sign in front of them that says, “Danger!” These are areas that developers get into and then write to MacDTS for help. Notation Conventions -------------------- All global variables begin with a lower case “g”. All constants begin with “k” except for those noted here. Resource IDs begin with “r”. Menu IDs being with “m” and items with an “i”. Resource strings begin with “s”. Human Interface --------------- This is the most important and about the most difficult aspect of programming on the Macintosh. In the development of SoundApp I gave much thought to the human interface issues. In fact, in talking with the Human Interface Group additions to the existing guidelines were made. The method of window stacking used here was a new one. This was documented in a Human Interface Tech Note. I even made one compromise (hard to believe!) suggested by the Human Interface Group. I originally had the buttons and the list in my choice of font and size. They felt that buttons should be in the System font and the list should also be the same. I liked my font choice better, but the group had a point that I really couldn’t argue with. That was, “If there isn’t a compelling reason to change something standard, then don’t change it.” Buttons on a Macintosh typically appear in the System font. Changing the font, just because I wanted to, was considered gratuitous. Standard File is in the System font and it also contains a list and buttons. Since my window are very similar to that dialog, I’m using the System Font. SoundApp is never modal unless an error occurs and I need to show and alert. Controls are inactivated for inactive windows. The default button is given the proper outline, and this outline disappears when the window is deactivated. Keyboard equivalents for the buttons cause the button to appear as if it had been clicked in. The check box in the Standard File dialog remembers the user’s last setting. The about box is only semi-modal. It will allow the user access to switch to another application. The status window under some circumstances was found to disappear too quickly, so a built in delay was added. Windows are centered or stacked according to the Human Interface Guidelines. The sound level isn’t adjusted by the application, and instead the users is informed of the current level and told how it can be adjusted. The About box ------------- It’s rad. Has a color icon, shows the 'vers' resource, goes Moof!™. It also demonstrates how to handle a modal window without the Dialog Manager. This technique can be use for any window, including dialog windows. The dialog’s update routine would call UpdtDialog. The really new point to notice is this window is modal but ONLY within the application’s layer. While running under MultiFinder, the user can switch to other applications. While the About window is present, it is the only window belonging to the application that accepts user actions. Memory Management ---------------- This has to be the most difficult portion of a Mac application to write. This along with the user interface. I spent too many nights chasing down crashes while running the application under low memory. I found unpleasant surprises while doing this. The Sound Manager doesn’t check for NIL pointers. OpenResFile may take large portions of my heap away. The toolbox seems to need at least 32k of free space in the heap of my Mac II running color. I wrote a simple grow zone procedure that will dump a reserve memory block. This is only considered for use in an emergency. I never rely on using it directly. If the reserve has been released, I will not continue an operation such as playing a sound or showing the status window until it is regained. Grow zones should not be considered a solution to memory management. They can be used to augment your overall memory management scheme. Error Checking -------------- Lots and lots and lots of it. I could even do more. Programmers need to do more of this. The Sound Manager will crash when encountering a NIL pointer. My application should never crash. If you can find a way to crash this application, then I want to hear about it. Using a bogus 'snd ' resources doesn’t count and I’ve found many of those. Writing proper error checking into the code during development really helped. Always handle errors, and pass along the error. I will let the first error encountered to be passed all the way up to the caller and eventually my error dialog will show up for the user. SetPort Strategy ---------------- Any routine that needs to use Quickdraw will set the port. I do not believe that it should also be responsible for restoring the port back to what it may have been before the routine was called. So, you’ll find there is an absence of the GetPort, SetPort, do my thing, and then SetPort again. Instead I SetPort and do my thing. The Mac often is setting the port unnecessarily. Strings ------- All of my strings are resources. There are no strings that appear within the code. This helps memory management and allows me to adjust the application to international systems without compiling any code. I could simply use ResEdit, or some other tool, to localize this application. I find it is just as easy to run Rez again than attempting to use ResEdit. Besides, after editing with ResEdit I want the source for that and would have to run DeRez which isn’t nearly as clean as my original source files. Window Stacking --------------- I hate applications that will open a new document that covers up an existing document, unless the new document covers the entire screen. So, my application’s documents have a small window size. I wanted to open new windows that would not cover up older ones. This is nice for the user, since they will not have to move windows just to get at other documents. ResEdit will stagger new windows off of the frontmost window but I find that this isn’t the best approach. It will still cover up other windows and I also don’t like windows that will open half way between two monitors. I wanted a better approach: one that would always stagger new windows and not cover up older ones. When I want to center a window, I need to know its entire rectangle size. The rub is that I cannot determine its size until I show it because I only know about the window’s boundsRect. This does not include the area that contains the title bar. That’s the strucRgn, which is an empty region for an invisible window. I could do what MacApp does, but if I have to do another thing that MacApp already does I’ll give up and stick with MacApp. I ended up writing a routine that takes a guess at the height of the window’s title bar. This is another thing that was harder than it should have been. Dialog Manager (and some of the reasons I don’t like it) -------------------------------------------------------- My first approach was to use modeless dialogs for document windows, thinking that I could write an application that would demonstrate how to deal with them and all of their idiosyncrasies. Not long into the development cycle it became obvious to me that I was fighting something that contained more disadvantages than advantages. I removed all the dialog code and only used standard windows and controls the old fashion way. In the case of the About window, which is semi-modal, I have a test that will return TRUE for a window that should be treated as modal. This allows my window to be handled by my standard event handlers and I don’t have to write dialog filters. There are some things that do not get handled properly while calling ModalDialog. ModalDialog ignores disk insert events. The activate or update events do not get handled for background windows. Using a modeless dialog fails with MultiFinder if switching takes place while the dialog is the frontmost window. The problem is that DialogSelect ignores and removes the suspend/resume event. Another advantage to all this is that drawing was much faster. As an example of some of the problems with ModalDialog and the activate event. Try this with the Finder. Open a window and choose “View by name.” Then select a few names with the shift key and resize the window so the vertical scroll bar is visible. Move this window to one edge of the screen or a second monitor. Now choose “Set Startup.” This is a modal dialog. If you look at the Finder window with the selected files, you’ll notice that the scrollbar and the text are still highlited. This is not the proper user interface. This is because the deactivate routines are not called while in ModalDialog. You can even find this problem with SoundApp. On deactivate events I will change my controls to the inactive state. If you place the buttons to the side of the screen and then bring up the standard file dialog, you’ll notice that the buttons don’t change properly. ModalDialog also prevents the application from updating background windows too. To solve this a dialog filter procedure is required. In most cases, this filter would be as complex and the event loop. It would also make it necessary to call your event routines from outside of the normal event loop. All on this isn’t worth the effort. You can see how this does not happen while using this application’s About window. Select an item in the document window and choose “Play Melody.” This will leave the status window on screen so that you can drag it to cover the document window. Now select “About SoundApp” to bring up the about window. This causes the status window to close, which uncovers the document window leaving an invalid area. The document window gets an activate event, then the About window appears. Then the document window is properly deactivated and updated. Yeah, just like it should happen. So, the tradeoff was that I didn’t have to work around all the strange things the Dialog Manager does such as running a secondary event loop, and requiring me to have userItems or filterProcs. This made the code smaller, more readable, and faster. I think I will avoid the Dialog Manger from now one unless I’m using a very simple dialog. The about window of this application proved too much for the Dialog Manager. One thing dialogs are good for is running ResEdit and laying out the dialog. To help position controls, I used a DLOG resource of the same size as my WIND resource. The DITL of this dialog contains the positions I wanted for my CNTL resources. This helped me to look at where I could expect my buttons to show up. This is one of the main reasons people think they need the Dialog Manager, because ResEdit makes it easy to build dialogs. ResEdit alone has contributed to nearly all of the Dialog Manager abuse in the world today. I used a Rect resource for positioning the list rectangle of the document windows. These windows look very much like a modeless dialog. (They used to be, but that presented to many problems.) The About window is also a standard window, but shown modally. Just like ModalDialog, but my modal window does allow switching under MultiFinder. You can change the window to a dBoxProc and then MultiFinder will not switch while this is the active window. To help with the layout of the about window, I position the text within it based on the size of the window. The status window does this too. These two things, the Rect resource and text based on the size of the window, help when changing the text. If the new text doesn’t fit, then resize the window’s resource. I used some trick with Rez to help layout my window contents. Refer to the SoundApp.r sources. I’ve read and understood Tech Note 203, and have learned how to apply it. Bo3b Johnson is a smart guy, and developers should trust his opinions. List Manager ------------ It’s very easy to be tempted by this part of the toolbox, along with the Dialog Manager. The List Manager is a slow beast at times. It also has some problems with “doing the right thing.” I’ve found that the list will not be updated properly when the user clicks in a cell that is out of bounds. LClick will return TRUE with a cell that doesn’t exists. LActivate will erase the scrollbars instead of highlighting the properly. Finally, the List Manager does not return errors. How would a person know if LSetCell worked? I’ve read and understood Tech Note 203, and have learned how to apply it. Resource Manager ---------------- I test all the handles being returned from the Resource Manager before using them, and if I get a NIL then I look at ResError. ResError sometimes lies and returns noErr and a NIL handle. ResError is usually good for getting an error code AFTER you’ve already found an error. Opening a resource file that is already open by another application is dangerous. The Resource Manager will not tell you when you’ve done this. There needs to be a OpenRFPerm that will return permission errors such as resFileBusyErr. Refer to Tech Note 185. When I or the Toolbox needs to get at one of my resources, CurResFile must be set to my application. Also, look out for one particularly nasty situation when switching resource files. If the segment loader goes for a CODE segment, it better be from our resource file! The idea here is, in case you didn’t get it already, always have the current resource file be set to the application. If a resource is needed from another file, switch momentarily to get the resource and immediately restore the current resource file to the application. I take an added measure of defense and whenever I need a resource I use the Get1Resource calls. These will only search the current resource file. Strategies For Sound -------------------- All of the Sound Manager code is contained in the SoundUnit.p. This code was written to be general purpose, providing useful routines for other applications. Lots of error checking is performed. I’ve also extended the support for SndPlay and made it really asynchronous. I’ve demonstrated most of the abilities the present Sound Manager has to offer. I will have to revise the SoundAppUnit to include any new features (e.g., multi channel support) when the next Sound Manager is released. I allocate my own memory to be used as sound channels. I allocate these pointers early in the application’s startup time to avoid memory fragmentation. These channels are of the standard size (holding 128 commands) but I’ve extended the structure to include my own information. When I create a new sound channel, I pass it a pointer to this memory. This will link in the 'snth' resource and hardware to my channel. When I dispose of the channel, the Sound Manager will purge this resource and disconnect me from the hardware. When adding the 'snth' resource, the Sound Manager will allocate a pointer into the application’s heap instead of the system’s. This is a modifier stub used by the 'snth'. This could cause some problems with memory management. I create and dispose of all my channels as soon as possible, and this doesn’t cause me problems. I keep track of which document is playing a sound, along with a global of when the application is playing sound. I needed to keep track of which document is playing because if the user disposes of that document, I will have to stop playing the sound contained in it since the user wants to dispose of that data. I keep track of when the application is playing sound in a global. This is only used by the routine that calculates the sleep time for WaitNextEvent. I came up with a pretty sick music notational system using Rez. Refer to the notes in the SoundAppSnds.r file. If you’ve just finished a meal, wait four hours before reading. The SoundUnit handles all of the Sound Manager code entirely. This eliminates any and all references to the Sound Manager from the application. The SoundUnit will return any error encountered while calling the Sound Manager, and does some extra error checking the Sound Manager doesn’t do. The portion of the application that uses the wave table synthesizer is more complex than the other two. I wanted to include an example channel modifier for use in the wave table channels. This would have been a transpositional modifier that would take a given freqDurationCmd and transpose it by some amount. This would be nice for the routine that plays a scale, by allowing the other three channels to be playing the same scale but at a different interval. Unfortunately, I found that the Sound Manager has bugs using a modifier, at least with the wave table synths, and could not use them. I’ve created a few wave table sounds and keep them in a 'snd ' resource. This allows me to change the sound of the wave table channels and not change any of the code. Creating wave table data is complicated. The example sounds I’ve included are samples I’ve taken from various sources. I’ve cleaned them up quit a bit. This was to set loop points, try and reduce clicks, correct the sample rates, and base frequencies. This is also a complicated task. Maybe I should document these techniques. Jim Reekes E.O., Macintosh Developer Technical Support Sunday, August 7, 1994 7:06:41 PM */ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // global defines // All of this code is for System 7 and later #define SystemSevenOrLater 1 // I don't use any obsolete stuff in the interfaces, and you shouldn't either. // You also need to worry about names because the Code Fragment Manager binds // system calls by name. Read the comments in ConditionalMacros.h #define OLDROUTINENAMES 0 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // includes //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #include <AppleEvents.h> #include <Controls.h> #include <CursorCtl.h> #include <Devices.h> #include <DiskInit.h> #include <Dialogs.h> #include <Errors.h> #include <Events.h> #include <Files.h> #include <Icons.h> #include <Gestalt.h> #include <Lists.h> #include <LowMem.h> #include <Menus.h> #include <Memory.h> #include <QuickDraw.h> #include <Resources.h> #include <Scrap.h> #include <Script.h> #include <SegLoad.h> #include <StandardFile.h> #include <TextUtils.h> #include <ToolUtils.h> #include <Traps.h> #include <Types.h> #include <Windows.h> #include <limits.h> #include <string.h> #include <Sound.h> #include <SoundInput.h> #include "SoundUnit.h" //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // constants //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ enum { kOSTrapBit = (1<<11), /* bit 11 is the OS Trap bit */ kTrapNumberMask = 0x07FF, /* bits used by the A-Trap mechanism */ kPollingSleepTime = 60, //MultiFinder’s sleep while sound is playing kNumberOfMasters = 3, //number of master pointer blocks we expect kSizeOfReserve = (unsigned long)32 * 1024, //size of reserve memory for grow zone proc kMinSpace = (unsigned long)32 * 1024, //minimum available memory I allow in heap kMemForSndDoc = 20 * 1024, //minimal amount of memory needed by document rAppSignature = 'SAPP', //applicaiton’s OS signature rSndAppDocType = 'sDoc', //document's file type kScrollbarAdjust = 16-1, //the width of the scrollbar in the list kListFrameInset = -1, //inset rectangle adjustment for list frame kStadardWhiteSpacing = 13, //inset rectangle adjustment for dialog items kRecordTop = 40, //40,50 is topLeft for SndRecord dialog kRecordLeft = 50, //40,50 is topLeft for SndRecord dialog kHiliteControlSelect = 1, //select the control kHiliteControlDeselect = 0, //deselect the control kCntlOn = 1, //control’s value when truned on kCntlOff = 0, //control’s value when truned off kWindowPosStartPt = 2, //offset from the topLeft of the screen kWindowPosStaggerH = 16, //staggering amounts for new windows kWindowPosStaggerV = 3, //not including the window’s title height kMinWindowTitleHeight = 19, kButtonFrameSize = 3, //button’s frame pen size kButtonFrameInset = -4, //inset rectangle adjustment around button kButtonSizeH = 17, //standard height of buttons kDafaultButSizeH = kButtonSizeH - kButtonFrameInset - kButtonFrameInset }; //refer to the SoundApp.r file for the explaination about these numbers enum { kNumOfButtons = 6, //the number of buttons in the window kSndButtonSizeW = 100, //SizeW of buttons in document window kSndButtonSizeH = 22, //heigth of buttons in document window kSoundWindowSizeH = ((kNumOfButtons * (kStadardWhiteSpacing + kSndButtonSizeH)) + kStadardWhiteSpacing), kSoundWindowSizeW = 296, //standard position for icons in alerts kIconWidthOrHeight = 32, kStdAlertIconTop = kStadardWhiteSpacing, kStdAlertIconLeft = 23, kStdAlertIconBottom = kStdAlertIconTop + kIconWidthOrHeight, kStdAlertIconRight = kStdAlertIconLeft + kIconWidthOrHeight, kFSAsynch = true, //asynchronous File Manager call kEnterKey = '\3', //the keys I’m looking for #ifdef applec kReturnKey = '\n', //MPW C is wrong, the return key is "\r" not "\n" #else kReturnKey = '\r', #endif kEscape = '\33', kUpArrow = '\36', kDownArrow = '\37', kPeriod = '.', kBackspace = '\b', //This bit set in the ioFlAttrib field if the file’s resource fork is open. kResForkOpenBit = (1 << 2), //For the delay time when flashing the menubar and highlighting a button. kDelayTime = 8, //8/60ths of a second //The minimal number of ticks the ShowWindow needs to be visible. kShowTimeDelay = 20, //BUG NOTE: timbreCmd value of 255 on the Mac Plus/SE will cause a crash Sound Manager 1 kSquareWave = 240, //square wave for squareWaveSynth kSineWave = 0, //sine wave for the squareWaveSynth kPreferredTimbre = 190, //my preferred timbre for the squareWaveSynth //Application snd resources must be higher than this number. kSystemSndRange = 8191 //This is the highest snd id reserved by Apple. }; //The following constants are the resource IDs. enum { rMenuBar = 1000, //application’s menu bar rExitAlert = 1000, //emergency exit user alert rUserAlert = 1001, //error message user alert rSoundVolAlert = 1002, //sound is set low alert rSaveAlert = 1003, //save changes? dialog rGetNameDLOG = 2000, //get a name for the sound dialog rNameItem = 3, //edit text item in rGetNameDLOG rUserItem = 5, //user item to help draw default outline rCustomGetFileDLOG = 2001, //dialog template for CustomGetFile rSndOnlyCheckBox = 10 //dialog item number in CustomGetFile }; /* These are the window IDs used in the applications. Each one must be unique, since they are used to identify which window the event took place in. */ enum{ rAboutWindow = 1000, //about window rStatusWindow = 1001, //sound status window rSoundWindow = 1002, //sound document window rListRectID = 1000, //resource containing size of list rectangle rCancelCntl = 1000, //stop button ID for the status window rPlaySndCntl = 1001, //sound button IDs for the sound document window rHyperPlayCntl = 1002, rPlayScaleCntl = 1003, rMelodyCntl = 1004, rStopCntl = 1005, rRecordCntl = 1006, rAboutOkCntl = 1007, rUntitled = 1000, //string ID for untitled resources rAboutText = 1001, //string ID for text appearing in about window rPutFileMsg = 1002, //string for text appearing in SFPutFile dialog rMoofIcon = 1000, //resource ID for ICON of application rAppPict = 1000, //resource ID of picture shown in about window rSndCursor = 1000 //cursor resource for our documents }; /* The following are the snd resource IDs contained in the application, which start at ID 9000. IDs 0 - 8191 are reserved for Apple. */ enum { rMoofSound = 9000, //snd for the about window rScaleSnd = 9001, //snd containing a scale rMelodyPart1 = 9002, //snd containing a melody rMelodyPart2 = 9003, //snd containing the harmony rMelodyPart3 = 9004, //snd containing the harmony rMelodyPart4 = 9005, //snd containing the harmony rWaveHarmony = 9006, //snd containing waveTable for harmony rWaveMelody = 9007, //snd containing waveTable for melody rCounterPt1 = 9008, //snd containing soprano part of counter point rCounterPt2 = 9009, //snd containing alto part of counter point rCounterPt3 = 9010, //snd containing tenor part of counter point rCounterPt4 = 9011, //snd containing bass part of counter point rSopranoVox = 9012, //snd containing waveTable of soprano voice rAltoVox = 9013, //snd containing waveTable of alto voice rTenorVox = 9014, //snd containing waveTable of tenor voice rBassVox = 9015, //snd containing waveTable of bass voice rSampleHarmony = 9016, //snd containing the sampled sound harmony rSampleMelody = 9017 //snd containing the sampled sound melody }; //The following are resource IDs for messages. enum { sErrStrings = 1000, //error string STR# ID sStandardErr = 1, //An error has occurred. sMemErr = 2, //A Memory Manager error has occurred. sResErr = 3, //A Resource Manager error has occurred. sCurInUseErr = 4, //That file is currently in use. sWavesBroken = 5, //The wave table synthesizer is not available. sWrongVersion = 6, //This system does not support the Sound Manager... sLowMemory = 7, //Memory is too low to continue... sNoMenus = 8, //Could not find application’s menu resources. sInitSoundErr = 9, //Could not initialize the SoundUnit. sSoundErr = 10, //The Sound Manager has encountered an error. sNewDocErr = 11, //Could not create a new document. sInitStatusErr = 12, //Error initializing the status window. sEditErr = 13, //Could not complete the edit command. sDocErr = 14, //There is a problem with this document. sMsgStrings = 1001, //message string STR# ID sPlayingMsg = 1, //playing a sound sHyperMsg = 2, //playing a sound the Hyper way sScaleMsg = 3, //playing scale sMelodyMsg = 4, //playing melody sTimbresMsg = 5, //playing various timbres sCounterPtMsg = 6, //playing 4 part counter point sSMErrStrings = 1002 //strings describing Sound Manager errors }; /* The following constants are used to identify menus and items. The menu IDs have an “m” prefix and the item numbers within each menu have an “i” prefix. */ enum { mApple = 128, //Apple menu and items iAbout = 1, mFile = 129, //File menu and items iNew = 1, iOpen = 2, iClose = 4, iQuit = 12, mEdit = 130, //Edit menu and items iUndo = 1, iCut = 3, iCopy = 4, iPaste = 5, iClear = 6, mDemos = 1000, //Demos menu and items iCheckVolume = 1, iSquareScale = 3, iSquareMelody = 4, iSquareTimbre = 5, iWaveScale = 7, iWaveMelody = 8, iWaveSATB = 9, iSampleMelody = 11, iSampleSATB = 12 }; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // macros //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Define HighWord and LowWord macros for efficiency. #define HighWord(aLong) ((aLong) >> 16) #define LowWord(aLong) ((aLong) & 0x0000FFFF) // accessing a rectangle's points #define TopLeft(r) (* (Point *) &(r).top) #define BottomRight(r) (* (Point *) &(r).bottom) #define AbsoluteValue(n) ((n > 0) ? n : -n) #if GENERATINGCFM #define CreateRoutineDescriptor(info, proc) \ RoutineDescriptor g##proc##RD = BUILD_ROUTINE_DESCRIPTOR(info, proc) #define GetRoutineAddress(proc) (&g##proc##RD) #else #define GetRoutineAddress(proc) proc #endif //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // types //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ typedef RectPtr *RectHandle; /* This is a document layout that contains the window record and references to the associating data. The window record MUST be the first field. This is because I use the window pointer returned by the Toolbox to be a pointer to my document. To confirm that the window pointer is a document pointer, I store an application reference in the window record’s refCon. Then, I use a routine to test for the presence of this reference to insure I’m looking at one of my document’s windows. */ struct SndDocument { WindowRecord window; // must be first field short resFile; // document’s resource file short vRefNum; long dirID; ListHandle list; // document’s list of sounds Boolean sndInUse; // document is using a 'snd ' resource }; typedef struct SndDocument SndDocument; typedef SndDocument *SndDocPeek; // to peek at the document record // This is the status window layout. The concept here is similar to the // document type mentioned above. The message is a string handle used to // store the current message. struct StatusWindow { WindowRecord window; StringHandle message; // current text of status message long showTime; // time window was shown }; typedef struct StatusWindow StatusWindow; typedef StatusWindow *StatWindowPeek; // This is the about window layout. The concept here is similar to the // document type mentioned above. The comment is a string handle used to // store the current message. struct AboutWindow { WindowRecord window; Handle appPict; // handle to picture of app’s name Handle comment; // handle to string of about comments }; typedef struct AboutWindow AboutWindow; typedef AboutWindow *AboutWPeek; // This is the template to the WIND resource. I used it to load in the WIND // resource and then adjust the boundsRect. I also look at the procID to // determine if it has a title bar or drag region. struct WindowTemplate { // template to a WIND resource Rect boundsRect; short procID; Boolean visible; Boolean filler1; Boolean goAwayFlag; Boolean filler2; long refCon; Str255 title; }; typedef struct WindowTemplate WindowTemplate; typedef WindowTemplate *WindowTPtr, **WindowTHndl; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // inlines //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is a handy routine to copy pascal strings. It's smaller than the library call pstrcopy, and faster too. */ #if GENERATING68K #pragma parameter PStringCopy(__A0,__A1) void PStringCopy(StringPtr source, StringPtr destination) FIVEWORDINLINE (0x7000, 0x1010, 0x12D8, 0x51C8, 0xFFFC); // moveq #0,d0 // move.b (a0),d0 // loop move.b (a0)+,(a1)+ // dbra d0,loop #else #define PStringCopy(source, dest) strncpy(dest, source, StrLength(source) + 1) #endif //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // prototypes //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ short NumToolboxTraps(void); Boolean TrapExists(short theTrap); pascal long MyGrowZone(Size cbNeeded); Boolean LowOnReserve(void); void RecoverReserve(void); Boolean AllocateReserve(void); Boolean FailLowMemory(long memRequest); Boolean HasSelection(SndDocPeek sndDoc); OSErr GetSelection(SndDocPeek sndDoc, SndListHandle *sndHandle); void SelectNextCell(ListHandle list, Boolean next); void SelectSndCell(SndDocPeek sndDoc, short resID); OSErr InitSndList(SndDocPeek docPtr); Boolean OpenByApp(FSSpecPtr file, WindowPtr *window); Point GetGlobalMouse(void); short GetWTitleHeight(short variant); Point GetGlobalTopLeft(WindowPtr window); void MyParamText(StringHandle text, StringPtr cite0, StringPtr cite1); void CenterWindowRect(short variant, Rect *theRect); WindowPtr GetCenteredWindow(short id, Ptr p, WindowPtr behind); short CenteredAlert(short alertID); DialogPtr GetCenteredDialog(short id, Ptr p, WindowPtr behind); void DoButtonOutline(ControlHandle button); void SelectButton(ControlHandle button); void ActivateSndCntls(SndDocPeek sndDoc); Boolean IsDAWindow(WindowPtr window); Boolean IsDocWindow(WindowPtr window); Boolean IsModalWindow(WindowPtr window); void KillSound(void); Boolean DoCloseWindow(WindowPtr window); void AlertUser(short error, short messageID); void EmergencyExit(short message); void Terminate(void); void DrawStatusWindow(void); void ShowStatusWindow(short messageID); void InitStatusWindow(void); pascal void DoErrorSound(short soundNo); void CheckSoundVolume(void); void AddSndDocControls(WindowPtr window); Boolean PositionAvailable(Point newPt, short wTitleHeight); WindowPtr NewStackedWindow(short windID, Ptr windStorage); OSErr CreateSoundDoc(short resRef, FSSpecPtr file); void OpenSoundDoc(FSSpecPtr file); void NewSoundDoc(void); pascal Boolean SFFilter(CInfoPBPtr p, Boolean *sndFilesOnly); pascal short SFGetHook(short mySFItem, DialogPtr dialog, void *sndFilesOnly); void GetSoundDoc(void); void DrawAboutWindow(WindowPtr window); void DoAbout(void); void PlaySelectedSnd(SndDocPeek sndDoc, short message); void PlaySndSong(SndDocPeek sndDoc, short sndID); void PlaySquareSong(short sndID); void PlaySquareTimbres(void); void PlayWaveScale(void); OSErr GetSndSongs(short sndSongID1, short sndSongID2, short sndSongID3, short sndSongID4, SndListHandle *sndSong1, SndListHandle *sndSong2, SndListHandle *sndSong3, SndListHandle *sndSong4); OSErr InstallWaveSnds(SndChannelPtr chan1, SndChannelPtr chan2, SndChannelPtr chan3, SndChannelPtr chan4, short waveID1, short waveID2, short waveID3, short waveID4); void PlayWaveMelody(void); void PlayWaveSATB(void); void PlaySampleMelody(void); void PlaySampleSATB(void); pascal void DefaultOutline(WindowPtr window, short theItem); void GetSndName(Str255 sndName); OSErr AddSnd(SndDocPeek sndDoc, StringPtr sndNamePtr, SndListHandle sndHndl); void ClearSnd(SndDocPeek sndDoc); void DoRecordSound(SndDocPeek sndDoc); void AdjustMenus(void); void CopySnd(SndDocPeek sndDoc); void CutSnd(SndDocPeek sndDoc); void PasteSnd(SndDocPeek sndDoc); void DoMenuCommand(long menuResult); void DrawSndWindow(WindowPtr window); void DoKeyDown(char key, WindowPtr window); Boolean ListClick(SndDocPeek sndDoc, EventRecord *event); void DoSndDocClick(SndDocPeek sndDoc, EventRecord *event); void DoStatClick(StatWindowPeek statWindow, EventRecord *event); void DoAboutClick(WindowPtr window, EventRecord *event); void DoUpdate(WindowPtr window); void DoActivate(WindowPtr window, Boolean becomingActive); pascal OSErr QuitApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon); pascal OSErr OpenDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon); pascal OSErr PrintDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon); pascal OSErr OpenApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon); void DoEvent(EventRecord *event); void AdjustCursor(RgnHandle region); void EventLoop(void); void Initialize(void); void main(void); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Globals (The “g” prefix is used to emphasize that a variable is global.) //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* gMac is used to hold the result of a SysEnvirons call. This makes it convenient for any routine to check the environment. It is considered global information, anyway. */ SysEnvRec gMac; //set up by Initialize /* gReserveMemory is a handle of reserve memory used by my grow zone procedure. If memory is attempted to be allocated and fails, my grow zone will release this reserved block of memory. */ Handle gReserveMemory; /* gStatusWindow is the window that tells the user what’s happening. I show it while a sound is playing, and remove it as soon as the sound is complete. It is initialized once during startup, and from then on two routines are used to show and hide it. This demonstrates orchestrating multimedia, that sound/graphic idea, at least in a crude sort of way. */ StatWindowPeek gStatusWindow; /* gAppResRef is the application’s resource file reference. I need to save this since I can open other resource files. The current resource file is always gAppResRef unless I momentarily set it to another file to read its resources, and then immediately restore it back. */ short gAppResRef; //set up by Initialize /* gInBackground is maintained by our osEvent handling routines. Any part of the program can check it to find out if it is currently in the background. */ Boolean gInBackground; //maintained by Initialize and DoEvent /* We have to allocate our own QuickDraw globals when creating code for PowerMacs. MetroWerks declares "qd" in their runtime, so don't do that twice. */ #if GENERATINGCFM && !defined(__MWERKS__) QDGlobals qd; #endif /* This is necessary because of the Apple Event Manager. We used to be able to quit directly when the user choose Quit from the File menu. But because of the design of Apple Events, we cannot call ExitToShell from the quit handler. That event handler must set a flag and then check it in the event loop. */ Boolean gTerminate; // allocate the RoutineDescriptors for Power Mac toolbox calls #if GENERATINGCFM CreateRoutineDescriptor(uppGrowZoneProcInfo, MyGrowZone); CreateRoutineDescriptor(uppFileFilterYDProcInfo, SFFilter); CreateRoutineDescriptor(uppDlgHookYDProcInfo, SFGetHook); CreateRoutineDescriptor(uppUserItemProcInfo, DefaultOutline); CreateRoutineDescriptor(uppUserItemProcInfo, QuitApplicationEvent); CreateRoutineDescriptor(uppUserItemProcInfo, OpenDocumentsEvent); CreateRoutineDescriptor(uppUserItemProcInfo, PrintDocumentsEvent); CreateRoutineDescriptor(uppUserItemProcInfo, OpenApplicationEvent); #endif //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // IMPLEMENTATION //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* InitGraf is always implemented (trap 0xA86E). If the trap table is big enough, trap 0xAA6E will always point to either Unimplemented or some other trap, but will never be the same as InitGraf. Thus, you can check the size of the trap table by asking if the address of trap 0xA86E is the same as 0xAA6E. */ #pragma segment Initialize short NumToolboxTraps(void) { if ( GetToolboxTrapAddress(_InitGraf) == GetToolboxTrapAddress(_InitGraf + 0x0200) ) return (0x0200); else return (0x0400); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Check to see if a given trap is implemented. GetTrapAddress may screw up and wrap around into the trap table if you give it a toolbox trap that is out of range, so check for this first. */ #pragma segment Initialize Boolean TrapExists(short theTrap) { UniversalProcPtr trapAddress; if ( !(theTrap & kOSTrapBit) ) // is this an OS trap? trapAddress = GetOSTrapAddress(theTrap); else { // no, this is a tool trap theTrap &= kTrapNumberMask; // get the trap number if ( theTrap < NumToolboxTraps() ) // can this tool trap exist? trapAddress = GetToolTrapAddress(theTrap); else return (false); } return ( GetToolboxTrapAddress(_Unimplemented) != trapAddress ); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is a very basic grow zone procedure. My application keeps a reserve handle of memory in case the Memory Manager gets a request for some memory that is not available in my heap. If memory were to get tight (<32k), the Toolbox will crash the system, especially Quickdraw. Before releasing the reserve handle I make sure it isn’t the GZSaveHnd. This handle cannot be touched by the grow zone procedure. WARNING: The grow zone procedure will be called and A5 may not be valid. Read Tech Note #136 and 208 */ #pragma segment Main pascal long MyGrowZone(Size cbNeeded) { #pragma unused (cbNeeded) long theA5; long result; theA5= SetCurrentA5(); if (((*gReserveMemory) != nil) && (gReserveMemory != GZSaveHnd())) { EmptyHandle(gReserveMemory); result = kSizeOfReserve; //released this much memory } else result = 0; //this may release more memory theA5= SetA5(theA5); return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Before my application attempts to use more memory, I call this routine to check if I’m already using my reserve memory. If so, then I better prepare to die or get my reserve back. */ #pragma segment Main Boolean LowOnReserve(void) { return((*gReserveMemory) == nil); // empty handle is low reserve } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called from the event loop if LowOnReserve returns that I’m out of the reserve memory. This will recover the reserve memory block. If this fails, it will be called the next time through the event loop. */ #pragma segment Main void RecoverReserve(void) { ReallocateHandle(gReserveMemory, kSizeOfReserve); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called at startup time to allocate the reserve memory block used in the grow zone procedure. If I’m unable to obtain this reserve, then return false to signal a failure. */ #pragma segment Initialize Boolean AllocateReserve(void) { gReserveMemory = NewHandle(kSizeOfReserve); return(gReserveMemory != nil); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Call PurgeSpace and see if the requested amount of memory exists in the heap including a minimal amount of heap space. Also, if my grow zone’s reserve has been release, that’s considered a failure. I don’t perform any purging here. The Memory Manager will do this if it needs the space. This routine can be called with a memRequest == 0. This checks if the heap space is getting critical. */ #pragma segment Main Boolean FailLowMemory(long memRequest) { long total; long contig; PurgeSpace(&total, &contig); return((total < (memRequest + kMinSpace)) || LowOnReserve()); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This a simple test to see if the user has a currently selected list item. */ #pragma segment Main Boolean HasSelection(SndDocPeek sndDoc) { Cell aCell; SetPt(&aCell, 0, 0); return(LGetSelect(true, &aCell, sndDoc->list)); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a document, this will find the currently selected cell in the list. Then using this cell as an index number, I call Get1IndResource. This will return the proper handle. Since the List Manager is zero based and the Resource Manager isn’t, I have to add one to the index. BUG NOTE: GetIndResource will return a bogus handle if the index is not positive. */ #pragma segment Main OSErr GetSelection(SndDocPeek sndDoc, SndListHandle *sndHandle) { Cell aCell; OSErr result; result = noErr; SetPt(&aCell, 0, 0); if (LGetSelect(true, &aCell, sndDoc->list)) { if (aCell.v > -1) { // GetIndResource doesn’t like < 0 UseResFile(sndDoc->resFile); // only get our resources *sndHandle = (SndListHandle)Get1IndResource(soundListRsrc, aCell.v + 1); if (*sndHandle == nil) result = ResError(); // return any error UseResFile(gAppResRef); // restore our resource file } } return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is used to allow the user to select items from the list by using the arrow keys. Given a list and the direction, this will select the next cell. It will do nothing if we’re at the first cell and the user wants to do even higher (sped), or if we’re at the last cell and the user wants to go even lower (rent a clue bud). */ #pragma segment Main void SelectNextCell(ListHandle list, Boolean next) { Cell aCell; short lastItem; lastItem = (**list).dataBounds.bottom - 1; // bounds is 1 greater SetPt(&aCell, 0, 0); if (LGetSelect(true, &aCell, list)) { if ((next && (aCell.v < lastItem)) || ((!next) && (aCell.v > 0))) { LSetSelect(false, aCell, list); if (next) aCell.v = aCell.v + 1; else aCell.v = aCell.v - 1; SetPt(&aCell, aCell.h, aCell.v); LSetSelect(true, aCell, list); LAutoScroll(list); } } else // if no cells selected... LSetSelect(true, aCell, list); // select the first cell } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine will go through the list of snd resources looking for the one that has the resource ID matching the parameter passed in. First thing to do is deselect the current selection. Then once the matching resource is found, select that one and scroll it into view. One assumption made is that the resources and the list are both index in the same order. This is true for the sndDoc, since I built the list this way. */ #pragma segment Main void SelectSndCell(SndDocPeek sndDoc, short resID) { Str255 name; Cell aCell; Handle sndHndle; ResType rType; short testID; short index; short numSnd; index = 0; aCell.h = 0; aCell.v = 0; if (LGetSelect(true, &aCell, sndDoc->list)) LSetSelect(false, aCell, sndDoc->list); //deselect any cell UseResFile(sndDoc->resFile); //count only its resources numSnd = Count1Resources(soundListRsrc); //number of sounds available do { index++; SetResLoad(false); //don’t load any resources sndHndle = Get1IndResource(soundListRsrc, index); //only get snd from file SetResLoad(true); //back to normal resource operations GetResInfo(sndHndle, &testID, &rType, name); } while ((resID != testID) && (index < numSnd)); UseResFile(gAppResRef); //restore our resource file aCell.h = 0; aCell.v = index - 1; LSetSelect(true, aCell, sndDoc->list); LAutoScroll(sndDoc->list); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is used to create the list showing all the snd resources in the file. If anything goes wrong while attempting to get any of the resources, then I return false. I don’t want the document to continue if something goes wrong while reading a resource. If I do get the resource, then I add its name to the list in a new row. I’m assured that at least one resource is in the file before this routine is called. Untitled resources will get one. */ #pragma segment Open OSErr InitSndList(SndDocPeek docPtr) { short index; short resID; short newRow; short numSnd; Handle resHandle; StringHandle strHandle; ResType itsType; Str255 resName; Str255 untitled; Cell aCell; OSErr result = noErr; strHandle = (StringHandle)Get1Resource('STR ', rUntitled); if (strHandle != nil) PStringCopy(*strHandle, untitled); // save no name title else untitled[0] = 0; // at least an empty string UseResFile(docPtr->resFile); numSnd = Count1Resources(soundListRsrc); newRow = LAddRow(numSnd, 0, docPtr->list); for (index = 1; index <= numSnd; index++) { SetResLoad(false); // don’t load any resources resHandle = Get1IndResource(soundListRsrc, index); // only get snd from file SetResLoad(true); // back to normal operations if (resHandle != nil) { // only if I got the snd GetResInfo(resHandle, &resID, &itsType, resName); if (StrLength(resName) == 0) // if the snd isn’t named... PStringCopy(untitled, resName); // give it a name SetPt(&aCell, 0, index - 1); LSetCell(&resName[1], StrLength(resName), aCell, docPtr->list); } else { // resHandle == NIL result = resNotFound; // problem with resource file break; // get out of the loop } } UseResFile(gAppResRef); // restore our resource file return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* In the case that the user has attempted to open a resource file that is already open, this routine will scan my open documents for it. If I find that the requested file is already open by the application, then return true and its window pointer. The application can then simply select that window. If the resource file is open but not by this application, that’s a problem and I don’t use the resource file. Many problems with using an open resource file. Now if the Resource Manager would take care of this problem for us... */ #pragma segment Open Boolean OpenByApp(FSSpecPtr file, WindowPtr *window) { Str255 docName; WindowPtr testWindow; Boolean result; result = false; testWindow = FrontWindow(); while (testWindow != nil) { if (GetWRefCon(testWindow) == rSoundWindow) if ((file->vRefNum == ((SndDocPeek)testWindow)->vRefNum) && (file->parID == ((SndDocPeek)testWindow)->dirID)) { GetWTitle(testWindow, docName); if (EqualString(file->name, docName, false, true)) { *window = testWindow; result = true; break; } } testWindow = (WindowPtr)(((WindowPeek)testWindow)->nextWindow); } return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Get the global coordinates of the mouse. Get the global coordinates by calling GetMouse and LocalToGlobal. This assumes the current port is a valid graf port. When wouldn’t it be? */ #pragma segment Main Point GetGlobalMouse(void) { Point globalPt; GetMouse(&globalPt); LocalToGlobal(&globalPt); return(globalPt); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Try and determine the window’s title bar height. This isn’t easy, especially if the window is invisible. We all know how the standard Apple System WDEF works, at least at this date we do. I assume that method as shown below. I could do what MacApp does with the Function CallWDefProc. If the user has installed a replacement for the System WDEF, then it’s their problem to deal with. My method will work as long as Apple doesn’t change how the WDEF in System 6.0 calculates the title bar height. This will allow my application to work on international Macs that have a larger system font than Chicago. I check the window’s variant code for one that includes a title bar. I will have to change this routine to adjust for the modal-moveable window type, which hasn’t been defined yet. In this routine, I violate my rule about not using the GetPort, SetPort, SetPort sequence mentioned at the start of the file. Mostly, I do this because it’s not all that apparent that a routine called GetWTitleHeight will change the port, so I make sure that it doesn’t. */ #pragma segment Open short GetWTitleHeight(short variant) { FontInfo info; GrafPtr curGraf; GrafPtr wMgrPort; short wTitleHeight; short result; if ( (variant == documentProc) || (variant == noGrowDocProc) || (variant == zoomDocProc) || (variant == rDocProc) ) { GetPort(&curGraf); GetWMgrPort(&wMgrPort); //I need to know the font... SetPort(wMgrPort); //info in the System’s port GetFontInfo(&info); SetPort(curGraf); //restore current port wTitleHeight = info.ascent + info.descent + info.leading + 2; if (wTitleHeight < kMinWindowTitleHeight) wTitleHeight = kMinWindowTitleHeight; result = wTitleHeight; } else result = 0; //other window types have no title return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a window, this will return the top left point of the window’s port in global coordinates. Something this doesn’t include, is the window’s drag region (or title bar). This returns the top left point of the window’s content area only. In this routine, I violate my rule about not using the GetPort, SetPort, SetPort sequence mentioned at the start of the file. Mostly, I do this because it’s not all that apparent that a routine called GetGlobalTopLeft will change the port, so I make sure that it doesn’t. */ #pragma segment Main Point GetGlobalTopLeft(WindowPtr window) { GrafPtr theGraf; Point globalPt; GetPort(&theGraf); SetPort(window); globalPt = TopLeft(window->portRect); LocalToGlobal(&globalPt); SetPort(theGraf); return(globalPt); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* I’m using string pointers (not necessarily Memory Manager pointers) to the replacement strings. Using Str255 would copy the string to the stack, and then to the text handle. This way it is only copied once. */ #pragma segment Main void MyParamText(StringHandle text, StringPtr cite0, StringPtr cite1) { long offSet; char param0[2] = {'^', '0'}; char param1[2] = {'^', '1'}; long newLength; if (StrLength(cite0) > 0) offSet = Munger((Handle)text, 1, param0, sizeof(param0), (Ptr)&(cite0[1]), StrLength(cite0)); if (StrLength(cite1) > 0) offSet = Munger((Handle)text, 1, param1, sizeof(param1), (Ptr)&(cite1[1]), StrLength(cite1)); newLength = GetHandleSize((Handle)text) - 1; if (newLength > 255) newLength = 255; (*text)[0] = newLength; // string’s new length byte } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a window’s portRect, this routine will center the rectangle on the main device within the desktop region. This excludes the menu bar area. It follows the Apple Human Interface Guidelines for where to place a window centered on the screen. That is, 1/3th of the desktop shows above the window and 2/3ths below it. It returns a new rectangle set to the centered position. The top and left of this rectangle can be used with MoveWindow. This routine only considers the main device. CenterWindowRect could take a GDevice as a parameter to center the VAR rect with. This would also allow Alerts or dialogs that are closely associated with a document window to be centered relative to the monitor that contains that document. This is also one of the Human Interface Guidelines. MacApp 2.0 has a utility CalcScreenRect that you can borrow from to include this. WARNING: This routine may move or purge memory. */ #pragma segment Open void CenterWindowRect(short variant, Rect *theRect) { Point rectSize; short wTitleHeight; wTitleHeight = GetWTitleHeight(variant); //get title height SetPt(&rectSize, theRect->right, theRect->bottom + wTitleHeight); //get size of rect SubPt(TopLeft(*theRect), &rectSize); // include it in size theRect->top = ((qd.screenBits.bounds.bottom - qd.screenBits.bounds.top - GetMBarHeight() - rectSize.v) / 3) + GetMBarHeight(); //1/3th below menubar, centered horz theRect->left = ((qd.screenBits.bounds.right - qd.screenBits.bounds.left) - rectSize.h) / 2; SetPt(&BottomRight(*theRect), theRect->left, theRect->top); //return adjusted rect AddPt(rectSize, &BottomRight(*theRect)); theRect->top += wTitleHeight; //remove title height from rect } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a window ID, this routine will center the window’s rectangle before showing it on the main screen. This follows the Apple Human Interface Guidelines for where to place a centered window on the screen. If the window is closely associated with another window, considerations should be given to where that other window is located. If this other window is on a screen other then the main monitor, then it would be best to center the window on that other monitor. VERSION 1.1: There is a problem with GetNewWindow in the old Mac Plus and SE ROMS. It will call ReleaseResource on the 'WIND' resource within GetNewWindow. This make the handle invalid after the call. Therefore, before calling HPurge, we get the resource once again. */ #pragma segment Open WindowPtr GetCenteredWindow(short id, Ptr p, WindowPtr behind) { Rect newRect; WindowTHndl windTemplate; WindowPtr window; window = nil; //initialize result windTemplate = (WindowTHndl)(Get1Resource('WIND', id)); if (windTemplate != nil) { HNoPurge((Handle)windTemplate); newRect = (**windTemplate).boundsRect; CenterWindowRect((**windTemplate).procID, &newRect); (**windTemplate).boundsRect = newRect; window = GetNewWindow(id, p, behind); HPurge(Get1Resource('WIND', id)); } return(window); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given an Alert ID, this routine will center the alert’s rectangle before showing it on the main screen. This follows the Apple Human Interface Guidelines for where to place a centered window on the screen. If the Alert is closely associated with another window, considerations should be given to what the window is located. If this other window is on a screen other then the main monitor, then it would be best to center the Alert on that other monitor. VERSION 1.2: System 7 supports auto-positioning of windows and dialog, so this routine isn't being called anymore. I has been left in the sources for the readers that may be interested in knowing how it works. */ #pragma segment Open short CenteredAlert(short alertID) { AlertTHndl alertHandle; Rect alertRect; alertHandle = (AlertTHndl)(Get1Resource('ALRT', alertID)); if (alertHandle != nil) { HNoPurge((Handle)alertHandle); alertRect = (**alertHandle).boundsRect; CenterWindowRect(dBoxProc, &alertRect); (**alertHandle).boundsRect = alertRect; HPurge((Handle)alertHandle); } return(Alert(alertID, nil)); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a dialog ID, this routine will center the dialog’s rectangle before showing it on the main screen. This follows the Apple Human Interface Guidelines for where to place a centered window on the screen. If the dialog is closely associated with another window, considerations should be given to what the window is located. If this other window is on a screen other then the main monitor, then it would be best to center the dialog on that other monitor. VERSION 1.1: There is a problem with GetNewDialog in the old Mac Plus and SE ROMS. It will call ReleaseResource on the 'WIND' resource within GetNewWindow. This make the handle invalid after the call. Therefore, before calling HPurge, we get the resource once again. VERSION 1.2: System 7 supports auto-positioning of windows and dialog, so this routine isn't being called anymore. I has been left in the sources for the readers that may be interested in knowing how it works. */ #pragma segment Open DialogPtr GetCenteredDialog(short id, Ptr p, WindowPtr behind) { Rect newRect; DialogTHndl dlogTemplate; DialogPtr dialog; dialog = nil; //initialize result dlogTemplate = (DialogTHndl)Get1Resource('DLOG', id); if (dlogTemplate != nil) { newRect = (**dlogTemplate).boundsRect; CenterWindowRect((**dlogTemplate).procID, &newRect); (**dlogTemplate).boundsRect = newRect; HNoPurge((Handle)dlogTemplate); dialog = GetNewDialog(id, p, behind); HPurge(Get1Resource('DLOG', id)); } return(dialog); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given any control handle, this will draw an outline around it. This is used for the default button of a window. The extra nice feature here is that I’ll erase the outline for buttons that are inactive. Seems like there should be a Toolbox call for getting a control’s hilite state. Since there isn’t, I have to look into the control record myself. This should be called for update and activate events. The method for determining the oval diameters for the roundrect is a little different than that recommended by Inside Mac. IM I-407 suggests that you use a hardcoded (16,16) for the diameters. However, this only looks good for small roundrects. For larger ones, the outline doesn’t follow the inner roundrect because the CDEF for simply buttons doesn’t use (16,16). Instead, it uses half the height of the button as the diameter. By using this formula, too, our outlines look better. WARNING: This will set the current port to the control’s window. */ #pragma segment Main void DoButtonOutline(ControlHandle button) { Rect theRect; PenState curPen; short buttonOval; if (button != nil) { SetPort((**button).contrlOwner); GetPenState(&curPen); PenNormal(); theRect = (**button).contrlRect; InsetRect(&theRect, kButtonFrameInset, kButtonFrameInset); buttonOval = (theRect.bottom - theRect.top) / 2; if (((**button).contrlHilite == kControlNoPart)) PenPat(&qd.black); else PenPat(&qd.gray); PenSize(kButtonFrameSize, kButtonFrameSize); FrameRoundRect(&theRect, buttonOval, buttonOval); SetPenState(&curPen); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given the button control handle, this will cause the button to look as if it has been clicked in. This is nice to do for the user if they type return or enter to select the default item. */ #pragma segment Main void SelectButton(ControlHandle button) { long finalTicks; HiliteControl(button, kHiliteControlSelect); Delay(kDelayTime, &finalTicks); HiliteControl(button, kHiliteControlDeselect); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a sound document, this routine goes through the control list looking for a control that needs to be activated, or deactivated what ever the case maybe. The Play button is the default button, and it also needs its outline drawn. The Stop button is always active on the front most window. This is true even if there is no selection made in the window’s list. The Stop button is a global action, regardless of what is happening in the window. VERSION 1.1: Stop button is only active while a sound is playing. */ #pragma segment Main void ActivateSndCntls(SndDocPeek sndDoc) { ControlHandle control; long cntlRefCon; Boolean activate; activate = ((WindowPeek)sndDoc)->hilited && HasSelection(sndDoc); control = ((WindowPeek)sndDoc)->controlList; while (control != nil) { cntlRefCon = GetControlReference(control); if ( (cntlRefCon == rPlaySndCntl) || (cntlRefCon == rHyperPlayCntl) || (cntlRefCon == rPlayScaleCntl) || (cntlRefCon == rMelodyCntl)) { if (activate) HiliteControl(control, kControlNoPart); else HiliteControl(control, kControlInactivePart); if (cntlRefCon == rPlaySndCntl) DoButtonOutline(control); } if (cntlRefCon == rStopCntl) { if (((WindowPeek)sndDoc)->hilited && HasChannelOpen()) HiliteControl(control, kControlNoPart); else HiliteControl(control, kControlInactivePart); } //cntlRefCon == rStopCntl if (cntlRefCon == rRecordCntl) { if (((WindowPeek)sndDoc)->hilited) HiliteControl(control, kControlNoPart); else HiliteControl(control, kControlInactivePart); } //cntlRefCon == rRecordCntl control = (**control).nextControl; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Check if a window belongs to a desk accessory. This will first test for a nil window. DAs will have a negitive windowKind. */ #pragma segment Main Boolean IsDAWindow(WindowPtr window) { if (window == nil) return(false); else //DA windows have negative windowKinds return(((WindowPeek)window)->windowKind < 0); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Check to see if a window is a document window. This will first test for a nil window. Document windows all have a refCon set to rSoundWindow. This insures there is document type information after the window record. */ #pragma segment Main Boolean IsDocWindow(WindowPtr window) { Boolean result; if (window == nil) result = false; else result = (GetWRefCon(window) == rSoundWindow); return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This test will return true for a window that needs to be treated as a modal window. To include additional windows, add the window’s refCon value to the set of modal windows. */ #pragma segment Main Boolean IsModalWindow(WindowPtr window) { long wRef; Boolean result; if (window == nil) result = false; else { wRef = GetWRefCon(window); result = (wRef == rAboutWindow); // test for all our modal windows } return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is used to free up all the sound data and channels in use. There are two reasons for using this routine. One is after a sound has completed the other is to terminate a sound that may be in progress. For this second reason, this routine should always be used before playing a new sound. When disposing of a sound channel, this sets all document’s sndInUse flags to false. I can call this routine at any time. When I find that the memory reserve is used, I will call this to free up any possible sound resources which tend to be large. Another important point is the human interface issue of hiding the status window. It’s possible that playing some short sounds could cause the status window to disappear before the user had much of a chance to see it. To solve this, there is a delay that insures the status window was visible for a minimal time. VERSION 1.1: Update the document's controls since know that the sound is no longer playing the buttons in the document window need to be updated. */ #pragma segment Main void KillSound(void) { WindowPtr window; if (HasChannelOpen()) { // if a channel is open... window = FrontWindow(); // set all document’s flags while (window != nil) { if (IsDocWindow(window)) // if this is my document... ((SndDocPeek)window)->sndInUse = false; // then it is no longer active window = (WindowPtr)((WindowPeek)window)->nextWindow; } } if (HasSoundCompleted()) { // why were we called? DoSoundComplete(); // from completion // delay for status window to show a minimal amount of time while ((TickCount() - gStatusWindow->showTime) < kShowTimeDelay) {}; } else FreeAllChans(); // or just because HideWindow((WindowPtr)gStatusWindow); window = FrontWindow(); if (IsDocWindow(window)) ActivateSndCntls((SndDocPeek)window); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Close any of my windows. At this point, if there was a document associated with a window, you could do any document saving processing if it has been changed since being opened. DoCloseWindow would return true if the window actually closed. false would be returned, as an example, if the user chose “Cancel” in the Save As… dialog. An important detail is if the window to be close is one of my documents that is currently playing a sound. We cannot dispose of it yet, since the sound resource owned by the document would be in use by the Sound Manager. So, I test the document’s sndInUse flag and if it is in use I call KillSound. The About window contains some resource handles. I only mark them purgeable, but I could use ReleaseResource and then DisposHandle. */ #pragma segment Main Boolean DoCloseWindow(WindowPtr window) { Boolean result = true; switch (GetWRefCon(window)) { case rSoundWindow: if (((SndDocPeek)window)->sndInUse) // contain a snd in use? KillSound(); // not any more if (((SndDocPeek)window)->list != nil) // dispose of document data LDispose(((SndDocPeek)window)->list); if (((SndDocPeek)window)->resFile != 0) CloseResFile(((SndDocPeek)window)->resFile); CloseWindow((WindowPtr)window); DisposePtr((Ptr)window); // dispose of our doc storage break; case rStatusWindow: DisposeHandle((Handle)gStatusWindow->message); CloseWindow((WindowPtr)gStatusWindow); DisposePtr((Ptr)gStatusWindow); break; case rAboutWindow: if (((AboutWPeek)window)->comment != nil) // never dispose... HPurge(((AboutWPeek)window)->comment); // a Resource handle if (((AboutWPeek)window)->appPict != nil) HPurge(((AboutWPeek)window)->appPict); CloseWindow((WindowPtr)window); DisposePtr((Ptr)window); break; default: if (IsDAWindow(window)) // we can close DAs too CloseDeskAcc(((WindowPeek)window)->windowKind); break; } // switch GetWRefCon(window) return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Display an alert to inform the user of an error. MessageID acts as an index into a STR# resource of error messages. This will attempt to replace the error number with a string if the sound is a Sound Manager error. BUG NOTE: GetIndString will return a bogus string if the index is not positive. */ #pragma segment Main void AlertUser(short error, short messageID) { Str255 msg1; Str255 msg2; short theItem; UseResFile(gAppResRef); // restore our resource file if (messageID > 0) GetIndString(msg1, sErrStrings, messageID); else msg1[0] = 0; // case there’s no message if ((error <= noHardware) && (error >= badFormat)) GetIndString(msg2, sSMErrStrings, AbsoluteValue(error) + noHardware + 1); else NumToString(error, msg2); ParamText(msg1, msg2, "\p", "\p"); theItem = Alert(rUserAlert, nil); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Display an alert that tells the user an error occurred, then exit the program. This routine is used as an ultimate bail-out for serious errors that prohibit the continuation of the application. Errors that do not require the termination of the application are handled with AlertUser. Error checking and reporting has a place even in the simplest application. BUG NOTE: GetIndString will return a bogus string if the index is not positive. */ #pragma segment Main void EmergencyExit(short message) { Str255 msg; short theItem; SetCursor(&qd.arrow); if (message > 0) GetIndString(msg, sErrStrings, message); else msg[0] = 0; // case there’s no message ParamText(msg, "\p", "\p", "\p"); theItem = Alert(rExitAlert, nil); ExitToShell(); //gotta go! } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Clean up the application and exit. I close all of the windows so that they can update their documents, if any. Dispose of the SoundUnit and close the status window if the user really wants to quit. */ #pragma segment Main void Terminate(void) { WindowPtr aWindow; Boolean closed; closed = true; KillSound(); //stop any sound in progress do { aWindow = FrontWindow(); //get the current front window if (aWindow != nil) closed = DoCloseWindow(aWindow); //close this window } while ((closed) && (aWindow != nil)); //do all windows if (closed) { FreeSoundUnit(); //get rid of all sound equipment gTerminate = true; //exit if no cancellation } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Draw the status message and update the button. I setup a rectangle that is the text area. This is the entire window area, inset a small amount. I also subtract from the bottom of the rectangle the area used by the button. Doing things this way allowed me to change the size of the window and not change any of this code. The rectangle used by text will fill the window regardless of the new size I give it in the WIND resource. */ #pragma segment Main void DrawStatusWindow(void) { Rect theRect; PenNormal(); theRect = ((WindowPtr)gStatusWindow)->portRect; theRect.bottom = theRect.bottom - kDafaultButSizeH; InsetRect(&theRect, kStadardWhiteSpacing, kStadardWhiteSpacing); HLock((Handle)gStatusWindow->message); TETextBox((*(gStatusWindow->message) + 1), **(gStatusWindow->message), &theRect, teJustLeft); HUnlock(((Handle)gStatusWindow->message)); UpdateControls((WindowPtr)gStatusWindow, ((WindowPtr)gStatusWindow)->visRgn); DoButtonOutline(((WindowPeek)gStatusWindow)->controlList); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Whenever I start a sound, I show this status window. It will contain a message showing what is currently happening. A very good rule is not to perform drawing outside of the update event. I bend that rule here. I could simply select this window and show it. This would allow my normal drawing and activating routines to be called. The problem with that was, I found that many times the status window appeared and then disappeared before these update routines had a chance to draw the window. This left the user with a blank window that immediately went away. So, I do the activation and drawing right after showing it. BUG NOTE: GetIndString will return a bogus string if the index is not positive. */ #pragma segment Main void ShowStatusWindow(short messageID) { Str255 msg; if (FailLowMemory(0)) { // running low on memory? KillSound(); AlertUser(memFullErr, sLowMemory); } else { if (messageID > 0) GetIndString(msg, sMsgStrings, messageID); else msg[0] = 0; // case there’s no message SetString(gStatusWindow->message, msg); ShowWindow((WindowPtr)gStatusWindow); // show the window SelectWindow((WindowPtr)gStatusWindow); // bring it to the front SetPort((WindowPtr)gStatusWindow); EraseRect(&(((WindowPtr)gStatusWindow)->portRect)); // erase old message HiliteControl(((WindowPeek)gStatusWindow)->controlList, kControlNoPart); DrawStatusWindow(); // draw the new message ValidRect(&(((WindowPtr)gStatusWindow)->portRect)); // avoid needless update gStatusWindow->showTime = TickCount(); // it’s show time folks } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This initializes the status window, and keeps it hidden until the user decides to play a sound. I also center it for the first time, but the user is free to drag it to a new location afterwards. The status window also has a message associated to it, so I allocate a string handle for this. */ #pragma segment Initialize void InitStatusWindow(void) { WindowPtr window; ControlHandle stopButton; gStatusWindow = (StatWindowPeek)NewPtrClear(sizeof(StatusWindow)); if (gStatusWindow == nil) EmergencyExit(sInitStatusErr); window = GetNewWindow(rStatusWindow, (Ptr)gStatusWindow, (WindowPtr)-1); if (window == nil) EmergencyExit(sInitStatusErr); SetWRefCon(window, rStatusWindow); stopButton = GetNewControl(rCancelCntl, window); if (stopButton == nil) EmergencyExit(sInitStatusErr); gStatusWindow->message = NewString("\p"); // a new empty string handle if (gStatusWindow->message == nil) EmergencyExit(sInitStatusErr); HNoPurge((Handle)gStatusWindow->message); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* I just flash the menubar for a moment instead of playing any sounds. It’s the same effect as setting the sound volume to zero but that would prevent me playing my sounds. Also, it helps to prevent the problem of _SysBeep being called by the Dialog Manager while I’m playing a sound. If my application is playing a sound and, for example, the user clicks outside of the Standard File dialog ModalDialog calls _SysBeep. This can cause the Mac to crash or trash my channel. BUG NOTE: If the current Sound Manager were playing a sound and a _SysBeep were to occur, bad things could happen on a Mac Plus/SE. Either the application’s channel would be trashed or the Mac could crash. VERSION 1.1: The new Sound Manager will handle the problem presented in the older Sound Manager regarding SysBeep. If I'm running under the new Sound Manager then I'll call SysBeep anyway. The new Sound Manager will properly handle the situation of an open sound channel when SysBeep is called. VERSION 1.2: This is no longer needed since we only use Sound Manager 2 or later. */ #pragma segment Main pascal void DoErrorSound(short soundNo) { long finalTicks; if (GetSoundMgrVersion() == 1) { if (soundNo > 0) { //only after the first time FlashMenuBar(0); Delay(kDelayTime, &finalTicks); FlashMenuBar(0); } } else SysBeep(30); //does the right thing now } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This tests if the volume has been set very low. It’s a judgement call as to what “too low” means. The user sets the volume, so how could it be too low? A setting of 0 could be considered too low, but in any case do not adjust it. I find the volume goes below 4 on a standard Mac, then there is a perceivable difference. I also wanted to show this routine as a demonstration in how to handle such a situation. Do not change it directly! Ask the user to do it. Consider the user that has their Mac connected to a stack of Marshalls (which go to 11) and has purposely set their Mac’s volume where they wanted it. This may be 3 and if you crack it up to 8, it could be a scene out of Back to the Future. User Interface rule, let the user remain in control! VERSION 1.2: Don't use GetSoundVol anymore. This just reads a low memory global which is no longer valid with Sound Manager 3 or later. The old low global is only 3 bits, which is why the range used to be 0-7. There are new machines supported by Sound Manager 3 that have more than 8 volume level. So the old global is scaled into the 0-7 range. You should avoid all of this non-sense and start using the new calls of Sound Manager 3, and start using percentages. */ #pragma segment Initialize void CheckSoundVolume(void) { Fixed percent; long soundVolume; short curVolume; short item; Str255 numStr; if (GetSoundMgrVersion() < 3) { // turn old 0-7 range into the new 0-0x0100 range curVolume = LMGetSdVolume(); // same as calling GetSoundVol() curVolume *= kFullVolume; curVolume += 7-1; // avoid rounding errors curVolume /= 7; // divid by old max } else { // get the current stereo levels and average them GetDefaultOutputVolume(&soundVolume); curVolume = (soundVolume >> 16) + (soundVolume & 0x0000FFFF); curVolume /= 2; } // take the current level and turn this in to a percentage of full volume, // then show the user that the level is low and how they may increase it. percent = FixDiv( (long)curVolume << 16, kFullVolume << 16); percent = FixMul(percent, 100L << 16); NumToString(percent >> 16, numStr); ParamText(numStr, "\p", "\p", "\p"); // show the current volume item = Alert(rSoundVolAlert, nil); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Simply add all the buttons I want to use in the document window. I get the control templates, and add the control to the window. I don’t check for getting errors for two reasons. I put these controls there. If the user removes them, it’s their problem. If the application cannot get the memory required for these, then the application is already out of memory and checking here probably wouldn’t help much. I do check before creating the document that there was enough available memory. I save the resource ID into the control’s refCon so later I can tell which control was hit by the user. VERSION 1.1: Add the record button to the window.} */ #pragma segment Open void AddSndDocControls(WindowPtr window) { ControlHandle control; control = GetNewControl(rPlaySndCntl, window); if (control != nil) SetControlReference(control, rPlaySndCntl); control = GetNewControl(rHyperPlayCntl, window); if (control != nil) SetControlReference(control, rHyperPlayCntl); control = GetNewControl(rPlayScaleCntl, window); if (control != nil) SetControlReference(control, rPlayScaleCntl); control = GetNewControl(rMelodyCntl, window); if (control != nil) SetControlReference(control, rMelodyCntl); control = GetNewControl(rStopCntl, window); if (control != nil) SetControlReference(control, rStopCntl); control = GetNewControl(rRecordCntl, window); if (control != nil) SetControlReference(control, rRecordCntl); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #pragma segment Open Boolean PositionAvailable(Point newPt, short wTitleHeight) { WindowPtr oldWindow; Point delta; Point oldPt; Boolean taken; taken = false; oldWindow = FrontWindow(); while ((oldWindow != nil) && !taken) { if (IsDocWindow(oldWindow) && ((WindowPeek)oldWindow)->visible) { oldPt = GetGlobalTopLeft(oldWindow); delta.v = AbsoluteValue(newPt.v - oldPt.v); delta.h = AbsoluteValue(newPt.h - oldPt.h); taken = (delta.h + delta.v) <= ((kWindowPosStaggerH + kWindowPosStaggerV + wTitleHeight) / 2); } oldWindow = (WindowPtr)(((WindowPeek)oldWindow)->nextWindow); } return(!taken); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* There are a few approaches I thought of and tried, none of which satisfied me. ResEdit uses the simplest method, an offset from the front window. I don’t like this because it may cover up existing windows. So my approach is to go through the window list looking for an empty spot. If the empty spot causes the window to go off the screen, then I start a new stack off to the right. This will cause a new series of window to be stacked. If the new stack would cause the window to go off screen, then it’s time to give up. All new windows will then be moved to the starting point and there is no further stacking. If I look at this routine too much, I get real tired. If I try to change it at all, I get woozy trying to make it work again. VERSION 1.2: System 7 provides for staggering windows, but it doesn't work exactly the way I want it. Mine's better. */ #pragma segment Open WindowPtr NewStackedWindow(short windID, Ptr windStorage) { Point windSize; Point newPt; WindowPtr newWindow; short wTitleHeight; short numStacks; Boolean taken; //set the initail starting point for a window, less the staggering amount newWindow = GetNewWindow(windID, windStorage, (WindowPtr)-1); windSize = BottomRight(newWindow->portRect); SubPt(TopLeft(newWindow->portRect), &windSize); wTitleHeight = GetWTitleHeight(GetWVariant(newWindow)); SetPt(&newPt, kWindowPosStartPt - kWindowPosStaggerH, kWindowPosStartPt - kWindowPosStaggerV + GetMBarHeight()); numStacks = 0; do { //add the staggering amount then test if the window goes off the screen taken = true; SetPt(&newPt, newPt.h + kWindowPosStaggerH, newPt.v + kWindowPosStaggerV + wTitleHeight); if ( ((newPt.v + windSize.v) > qd.screenBits.bounds.bottom) || ((newPt.h + windSize.h) > qd.screenBits.bounds.right)) { numStacks++; SetPt(&newPt, kWindowPosStartPt + (numStacks * kWindowPosStaggerH), kWindowPosStartPt + wTitleHeight + GetMBarHeight()); if (((newPt.v + windSize.v) > qd.screenBits.bounds.bottom) || ((newPt.h + windSize.h) > qd.screenBits.bounds.right)) { SetPt(&newPt, kWindowPosStartPt, kWindowPosStartPt + wTitleHeight + GetMBarHeight()); taken = false; } } } while (taken && !(PositionAvailable(newPt, wTitleHeight))); MoveWindow(newWindow, newPt.h, newPt.v, true); return(newWindow); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Create a new sound document and initialize all the data associated with it. If I encounter an error, then dispose of any memory allocated in the attempt. Allocate all necessary memory required for a sound document. I like to use NewPtrClear because is will initialize all the memory. I depend on real values or zero in my document fields. Then its time to build a sound document which contains all the controls and the list of sounds. I get the position for the list kept in a rectangle resource. This helps me to adjust the size of the list without changing code. I will make an additional adjustment to the list’s height to insure better scrolling. This avoids the ugly white space of an improperly sized rectangle. If I cannot create the list, I’ll close the window and return a NIL. If I find after creating a new document that I’m low on memory, I’ll close it and return an error. SPECIAL NOTE: The Human Interface Group suggested that the buttons and the list should in the System font. I designed these windows using the application font in 10 point. If you change the call to TextFont, you find that the buttons and the list will work properly in your font choice. VERSION 1.1: Size the window smaller to hide the record button from the user if Sound Input is not available. Added memory check to the beginning of this routine before creating the document. This was done because there are two routines that call this one, OpenSoundDoc and NewSoundDoc which both performed this check. Having it here in one place reduces the chance of future bugs (doing the same thing in two places) and reduces the size of the code. */ #pragma segment Open OSErr CreateSoundDoc(short resRef, FSSpecPtr file) { SndDocPeek newDocPtr; WindowPtr window; ListHandle list; RectHandle rHandle; Rect lView; Rect lBounds; Cell aCell; Point cSize; Point newSize; short maxListHeight; OSErr theErr; Boolean ignore; if (FailLowMemory(kMemForSndDoc)) { CloseResFile(resRef); //close it before leaving return(memFullErr); } theErr = noErr; newDocPtr = (SndDocPeek)NewPtrClear(sizeof(SndDocument)); if (newDocPtr != nil) { window = NewStackedWindow(rSoundWindow, (Ptr)newDocPtr); SetPort(window); if (! HasSoundInput()) SizeWindow(window, kSoundWindowSizeW, kSoundWindowSizeH - (kStadardWhiteSpacing + kSndButtonSizeH), false); TextFont(0); //set window to System font, blech AddSndDocControls(window); //add my buttons SetWRefCon((WindowPtr)newDocPtr, rSoundWindow); //mark as an app window SetWTitle((WindowPtr)newDocPtr, file->name); newDocPtr->resFile = resRef; //save its resource file ref newDocPtr->vRefNum = file->vRefNum; //save its volume reference newDocPtr->dirID = file->parID; //save its directory ID newDocPtr->sndInUse = false; //not yet it doesn’t rHandle = (RectHandle)Get1Resource('RECT', rListRectID); if (rHandle != nil) { //get the stored list size lView = **rHandle; SetRect(&lBounds, 0, 0, 1, 0); //one dimentional list SetPt(&cSize, 0, 0); //List Mgr will find cell size list = LNew(&lView, &lBounds, cSize, 0, window, false, false, false, true); maxListHeight = window->portRect.bottom - window->portRect.top - (2 * kStadardWhiteSpacing); if (maxListHeight < (lView.bottom - lView.top)) maxListHeight = (lView.bottom - lView.top); if (list != nil) { newSize.h = (**list).cellSize.h; //get the width of one cell newSize.v = maxListHeight; //get the best height for all cells newSize.v -= maxListHeight % (**list).cellSize.v; LSize(newSize.h, newSize.v, list); //adjust for best scrolling (**list).selFlags = lOnlyOne; //single selections only newDocPtr->list = list; //save the list handle theErr = InitSndList(newDocPtr); //initialize the list data if (theErr == noErr) { SetPt(&aCell, 0, 0); //by default, I will... LSetSelect(true, aCell, newDocPtr->list); //select first item LSetDrawingMode(true, newDocPtr->list); ShowWindow((WindowPtr)newDocPtr); //get the show on the road if (FailLowMemory(0)) //if I’m low on memory... theErr = memFullErr; } } else theErr = nilHandleErr; //list handle was NIL } else theErr = nilHandleErr; //RECT handle was NIL if (theErr != noErr) { //could not create the list ignore = DoCloseWindow(window); //if not, close the window newDocPtr = nil; //return nil pointer too } } else //NewPtrClear was nil theErr = MemError(); return(theErr); //return the error, if any } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Believe it or not, but this turned out to be one of the more difficult routines to write. This was the best approach I could thing of to avoid serious problems. I don’t center the Standard File dialog because it wouldn’t be under the File menu where the user probably has the mouse. I use OpenRFPerm because it allows for permissions and doesn’t depend on the current directory. First problem with opening a resource file was to check if the file already being open. Read Tech Notes #116 and #185. GetFInfo seems to do the trick. At least it does report all files that are open on this machine. It may be incorrect for files on AppleShare opened by other machines. What I’m worried about is when opening a resource file for the second time on the same machine may return the previous opener’s resource map. This is very dangerous, and if the second opener calls CloseResFile there will be a crash. I want to be friendly and if the user tries to open the file the second time and I’ve already got it on the screen, then I’ll bring that window forward. To test for this, I compare the file’s name, DirID and vRefNum. The vRefNum is dynamic and needs to be converted into a volume name if I were to save this information for future use. There are a couple pitfalls, even with this method. The user can open the file, then move it to another directory. This same thing confuses all other applications I’ve tested. Opening a resource file will allocate a resource map handle into my heap, which may be large depending on the number of resources in that file. Additionally this loads all resources marked “preload.” So I set ResLoad to false to prevent the preloading. I test if the amount of memory I believe my document will require is available after opening the resource file. If true, I test if there are any sound resources in that file and if not alert the user. After all this, create the new document. If after creating the new document I find that memory is too low, will close it and return an error. If it does fail, then close the file before anything else or there may not be enough memory to show the alert. All of these tests created a number of IF-THEN-ELSE blocks and became unyielding. C programmers get a break, so gimme me one too. I use the MPW Pascal EXIT. BUG NOTE: Don’t open a resource file that is already open. OpenResFile may return an existing resource map when it gets opWrErr from the file system. If this happens, the resource file will not be unique and this is very bad. Another problem is if I get a read-only path and someone else opens it for read/write. This is also very bad. Read Tech Notes #116 and #185 hint at this problem, but I think a more comprehensive one is in order. BUG NOTE: GetWDInfo fails with nsvErr if the working directory returned from Standard File is the root of an A/UX volume. I could work around this, but it would be dependant on the current version of A/UX. Read Tech Note #229. I believe this is also true for TOPS. VERSION 1.1: I'll open files that do not have snd resources in them. This allows users to open existing files and then record or paste a sound into it. I changed OpenRFPerm to HOpenResFile to avoid working directories and to be consistent with the rest of the sources. I separated the standard file code from OpenSndDoc to support opening documents being open from the Finder. This allows me to share the same routine to open files either from double clicking them in the Finder, or by the Standard File dialog. Removed the checking for low memory conditions since CreateSoundDoc is now doing this. */ #pragma segment Open void OpenSoundDoc(FSSpecPtr file) { HParamBlockRec fPBRec; WindowPtr window; short resFileRef; OSErr theErr; fPBRec.fileParam.ioCompletion = nil; //prepare a paramBlock fPBRec.fileParam.ioNamePtr = file->name; fPBRec.fileParam.ioVRefNum = file->vRefNum; fPBRec.fileParam.ioDirID = file->parID; fPBRec.fileParam.ioFVersNum = 0; fPBRec.fileParam.ioFDirIndex = 0; theErr = PBHGetFInfoSync(&fPBRec); //fPBRec on stack, synch only if (theErr == noErr) { if (fPBRec.fileParam.ioFlAttrib & kResForkOpenBit) { if (OpenByApp(file, &window)) SelectWindow(window); //I opened this one, select it else AlertUser(noErr, sCurInUseErr); //in use by someone else return; } } else { AlertUser(theErr, sStandardErr); //PBGetFInfo failed return; } SetResLoad(false); //don’t load any resources resFileRef = FSpOpenResFile(file, fsCurPerm); theErr = ResError(); //save error, if any SetResLoad(true); //restore ResLoad state UseResFile(gAppResRef); //changes ResErr if (resFileRef != kResFileNotOpened) //error from OpenRFPerm? theErr = CreateSoundDoc(resFileRef, file); if (theErr != noErr) AlertUser(theErr, sNewDocErr); //couldn’t create new doc } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: Create a new file for the user. I prefer to use the new HCreateResFile to avoid working directories, and to be consistent with the rest of the sources. Also, I need the real vRefNum and dirID to store in the sndDoc record. After creating the file, I set its Finder information to the proper type and creator. Finally, I get to open the new file. After this point everything is exactly like opening an existing file and creating a new sound document. VERSION 1.2: If dupFNErr is returned by HCreateResFile, then the user wants to replace the file with a new SoundApp document. */ #pragma segment Open void NewSoundDoc(void) { Str255 msg; StandardFileReply reply; StringHandle strHandle; short resFileRef; OSErr theErr; strHandle = (StringHandle)Get1Resource('STR ', rPutFileMsg); if (strHandle != nil) PStringCopy(*strHandle, msg); //save no name title else msg[0] = 0; //at least an empty string StandardPutFile(msg, "\p", &reply); if (reply.sfGood) { theErr = noErr; if (reply.sfReplacing) theErr = FSpDelete(&reply.sfFile); //we're replacing it if (theErr == noErr) { FSpCreateResFile(&reply.sfFile, rAppSignature, rSndAppDocType, reply.sfScript); theErr = ResError(); if (theErr == noErr) { resFileRef = FSpOpenResFile(&reply.sfFile, fsCurPerm); theErr = ResError(); //save error, if any UseResFile(gAppResRef); //changes ResErr if (resFileRef != kResFileNotOpened) //error from HOpenResFile theErr = CreateSoundDoc(resFileRef, &reply.sfFile); } } if (theErr != noErr) AlertUser(theErr, sNewDocErr); //return the error } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is much more than what a typical file filter might do but I wanted the user to easily find files that contained snd resources. So, the global flag gSndFilesOnly is used to determine when to filter for such files. Otherwise, I show all files and allow the application to report to the user the file they’ve just tried to open doesn’t have any sounds to play with. Opening resource forks can be tricky if it’s already open. It would be very bad to use a resource fork that is already open by another application. The problem being that the Resource Manager doesn’t deal with multiple users. Read Tech notes #116 and #185. There’s also another problem. If a resource fork is opened by two applications and one closes the file then the entire resource fork may closed out from underneath the other application. I do, however, want to show the user files that contain sound resources even if they are currently open. I use HOpenResFile with read only permission, which will give me a unique resource reference. I look for a 'snd ' resource and then immediately close the file. Do not use a resource file opened with read only permission. Another reason to use HOpenResFile is to avoid a necessary working directory. I do not have one while in the file filter, but can use the DirID. Performing this search on each file is time consuming so I show a spinning cursor to show the user I’m working. Opening a resource fork may load resources mark preload. To avoid this, I call SetResLoad to false. I bet you thought the Resource Manager was a free lunch. Ha! Read Tech Note #203 for other reasons not to play with resources. BUG NOTE: While debugging this routine using heap scramble, I found that OpenResFile would not open the requested file. I’m not sure if this is a problem with OpenResFile or SFGetFile. VERSION 1.1: The bug mentioned above was found. The problem is that the paramBlock happens to be a re-locatable block in the heap. Passing the de-reference ioNamePtr to HOpenResFile was de-referencing this re-locatable paramBlock. Since HOpenResFile moves memory, the namePtr would no longer valid and thus HOpenResFile would fail. Now I copy the name out of the paramBlock and use the local variable in HOpenResFile. This problem was fixed in System 7, which no longer passes a re-locatable block to the file filter. The new version allows for any resource file to be opened. Because of this new feature, I only show the user files that have a resource fork. VERSION 1.2: This is a filter for System 7 now, and I don't show invisible files. */ #pragma segment Open pascal Boolean SFFilter(CInfoPBPtr p, Boolean *sndFilesOnly) { #define kShowIt false //false means I do not filter out... #define kDoNotShowIt true //the file and true means that I do. long oldTicks; short resRef; short curRes; Boolean result; oldTicks = TickCount(); result = kDoNotShowIt; //don’t show anything until I say so if ( (*sndFilesOnly) && !(p->hFileInfo.ioFlFndrInfo.fdFlags & fInvisible) ) { curRes = CurResFile(); SetResLoad(false); resRef = HOpenResFile(p->hFileInfo.ioVRefNum, p->hFileInfo.ioFlParID, p->hFileInfo.ioNamePtr, fsRdPerm); if (resRef != kResFileNotOpened) { UseResFile(resRef); if (Count1Resources(soundListRsrc) > 0) result = kShowIt; //hey, we found a sound in here CloseResFile(resRef); } //restore everything SetResLoad(true); UseResFile(curRes); } else { if ( !(p->hFileInfo.ioFlFndrInfo.fdFlags & fInvisible) // if not invisible && (p->hFileInfo.ioFlRLgLen > 0) ) // and has resources { result = kShowIt; } } RotateCursor(TickCount() - oldTicks); return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #pragma segment Open pascal short SFGetHook(short mySFItem, DialogPtr dialog, void *sndFilesOnly) { ControlHandle cntl; Rect r; short kind; short result; result = mySFItem; GetDialogItem(dialog, rSndOnlyCheckBox, &kind, (Handle *)&cntl, &r); switch (mySFItem) { case sfHookFirstCall: if (*(Boolean *)sndFilesOnly) SetControlValue(cntl, kCntlOn); else SetControlValue(cntl, kCntlOff); break; case rSndOnlyCheckBox: if (GetControlValue(cntl) == kCntlOff) { SetControlValue(cntl, kCntlOn); *(Boolean *)sndFilesOnly = true; } else { SetControlValue(cntl, kCntlOff); *(Boolean *)sndFilesOnly = false; } result = sfHookRebuildList; break; } return (result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine used to be inside of OpenSoundDoc. The standard file code was removed to here in order to support opening documents being open from the Finder. I've switched to using a constant for the topLeft point of the Standard File dialog. It saves a few bytes of code, so what the hell? I found a cosmetic problem with the cursor not being restored back to arrow after returning Standard File if there was an error dialog shown immediately. So, it is immediately set back to the arrow. */ #pragma segment Main void GetSoundDoc(void) { StandardFileReply reply; SFTypeList typeList; //not used, just a placeholder Point sfTopLeft; Boolean sndFilesOnly; SpinCursor(0); //get the spinning cursor ready sfTopLeft.v = -1; //-1,-1 means to center the dialog sfTopLeft.h = -1; sndFilesOnly = false; CustomGetFile((FileFilterYDUPP)GetRoutineAddress(SFFilter), -1, typeList, &reply, rCustomGetFileDLOG, sfTopLeft, GetRoutineAddress(SFGetHook), nil, nil, nil, &sndFilesOnly); SetCursor(&qd.arrow); if (reply.sfGood) OpenSoundDoc(&reply.sfFile); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Draw the contents of the about window in response to an update event. At this point, BeginUpdate has been called which sets the window’s visRgn to clip drawing only where it needs to be done. I have some text to draw, an icon, a picture, and a default button with its outline. If I have a color icon handle I’ll call PlotCIcon. I offset the picture to draw to the right of the icon. The text will appear in a rectangle as large as the window, but below the icon and above the button. This allows me the change the text and if needed change the size of the window’s rectangle to compensate. I won’t have to recompile any code. Some people use dialogs because of this, but I’m demonstrating how it can be done without them. I use UpdateControls to avoid needless drawing that happens with DrawControls. It not only runs faster but doesn’t flicker. VERSION 1.2: Uses System 7's PlotIcon to draw the proper icon. */ #pragma segment Main void DrawAboutWindow(WindowPtr window) { Rect theRect; OSErr ignoreErr; PenNormal(); SetRect(&theRect, kStdAlertIconLeft, kStdAlertIconTop, kStdAlertIconRight, kStdAlertIconBottom); ignoreErr = PlotIconID(&theRect, atNone, ttNone, rMoofIcon); theRect = (**(PicHandle)((AboutWPeek)window)->appPict).picFrame; OffsetRect(&theRect, -theRect.left + kStdAlertIconRight + kStadardWhiteSpacing, -theRect.top + kStdAlertIconTop); DrawPicture((PicHandle)((AboutWPeek)window)->appPict, &theRect); SetRect(&theRect, window->portRect.left, kStdAlertIconBottom, window->portRect.right, window->portRect.bottom - kDafaultButSizeH); InsetRect(&theRect, kStadardWhiteSpacing, kStadardWhiteSpacing); HLock(((AboutWPeek)window)->comment); TETextBox(*(((AboutWPeek)window)->comment) + 1, StrLength(*(((AboutWPeek)window)->comment)), &theRect, teJustLeft); HUnlock(((AboutWPeek)window)->comment); UpdateControls(window, window->visRgn); DoButtonOutline(((WindowPeek)window)->controlList); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* First thing to do is call KillSound to stop any sound in progress and close the status window. If there’s enough memory available, I’ll play the about sound. This is sound is asynchronous and will automatically be disposed of when completed. There is an icon, taken from the bundle resources, and a picture. The text is taken from a string resource. It contains a couple variables just like ParamText would want. I get the current name of the application from the AppParms. This is the name of the application as specified by the user. I read in my 'vers' resource to get the current version to be displayed in the window. Read Tech Note 189 for more details on the vers resource. I think these are two important things to show in the about box. These two portions of the text are put into place with my own version of ParamText. I tried all of this with a standard dialog, but had trouble. Sometimes the color icon didn’t show up. Trying to center justify text with a statText item wasn’t really possible. On the Mac SE, for some unknown reason, TESetJust failed to center the textH of the dialog. I also found that this call would set the dialog’s text back to Chicago even after I had called TextFont. So, I gave up and did everything myself. This is a demonstration of how to create a dialog without using the Dialog Manager. */ #pragma segment Main void DoAbout(void) { Str255 verNum; Str255 appName; Ptr aboutPtr; AboutWPeek aboutPeek; ControlHandle control; VersRecHndl curVersion; OSErr theErr; Boolean ignore; KillSound(); aboutPtr = NewPtrClear(sizeof(AboutWindow)); if (aboutPtr != nil) { aboutPeek = (AboutWPeek)GetNewWindow(rAboutWindow, aboutPtr, (WindowPtr)-1); SetWRefCon((WindowPtr)aboutPeek, rAboutWindow); curVersion = (VersRecHndl)Get1Resource('vers', 1); if (curVersion != nil) PStringCopy((**curVersion).shortVersion, verNum); // get version string else verNum[0] = 0; // at least initialize it PStringCopy(LMGetCurApName(), appName); aboutPeek->appPict = Get1Resource('PICT', rAppPict); aboutPeek->comment = Get1Resource('STR ', rAboutText); control = GetNewControl(rAboutOkCntl, (WindowPtr)aboutPeek); if ( (aboutPeek->appPict != nil) && (aboutPeek->comment != nil) && (control != nil)) { HNoPurge(aboutPeek->appPict); // must keep them around HNoPurge(aboutPeek->comment); MyParamText((StringHandle)aboutPeek->comment, appName, verNum); if (!FailLowMemory(0)) theErr = AsynchSndPlay((SndListHandle)Get1Resource(soundListRsrc, rMoofSound)); } else // couldn’t build window ignore = DoCloseWindow((WindowPtr)aboutPeek); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Get the selected sound and pass it to AsynchSndPlay or HyperSndPlay. This depends on the normal flag. HyperCard’s method is not normal. This is the HyperCard version of playing a sound. It resamples any sound to middle C. If the resource is too large to fit in memory, I’ll get a nil handle error from the SoundUnit. It might be nice to test for this before calling the SoundUnit and telling the user they’re too low on memory, but the SoundUnit is robust enough and reports the error. It’s important to call KillSound to dispose of any data that was allocated if an error were to occur. */ #pragma segment Main void PlaySelectedSnd(SndDocPeek sndDoc, short message) { SndListHandle sndHandle; OSErr theErr; theErr = GetSelection(sndDoc, &sndHandle); if (theErr == noErr) { if (message == sPlayingMsg) theErr = AsynchSndPlay(sndHandle); else theErr = HyperSndPlay(sndHandle); } if (theErr == noErr) { sndDoc->sndInUse = true; // this document has a snd in use ShowStatusWindow(message); } else { KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a sound resource ID, this will get my song resource and call the SoundUnit to use that sampled sound and song to play a tune. This application will play one of two songs for sampled sounds. If any error is encountered, I call KillSound to dispose of all data. */ #pragma segment Main void PlaySndSong(SndDocPeek sndDoc, short sndID) { SndChannelPtr chan; SndListHandle sndInst; SndListHandle sndSong; OSErr theErr; theErr = GetSelection(sndDoc, &sndInst); if (theErr == noErr) { sndSong = (SndListHandle)Get1Resource(soundListRsrc, sndID); theErr = ResError(); // save any error if (sndSong != nil) { theErr = GetSampleChan(&chan, kInitNone, sndInst); if (theErr == noErr) { sndDoc->sndInUse = true; // this document has a snd in use theErr = PlaySong(chan, sndSong); } } } if (theErr == noErr) { if (sndID == rScaleSnd) ShowStatusWindow(sScaleMsg); else // I can play scales or a melody ShowStatusWindow(sMelodyMsg); } else { // catch any errors KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a sound resource ID, this will get my song resource and call the SoundUnit to use the note synthesizer and song to play a tune. I request the note channel’s timbre (sounds like “tom burr”). If any error is encountered, I call KillSound to dispose of all data. */ #pragma segment Main void PlaySquareSong(short sndID) { SndListHandle sndSong; SndChannelPtr chan; OSErr theErr; sndSong = (SndListHandle)Get1Resource(soundListRsrc, sndID); if (sndSong != nil) { theErr = GetSquareWaveChan(&chan, kPreferredTimbre); if (theErr == noErr) { theErr = PlaySong(chan, sndSong); if (theErr == noErr) { if (sndID == rScaleSnd) ShowStatusWindow(sScaleMsg); else // I play scales or a melody ShowStatusWindow(sMelodyMsg); } } if (theErr != noErr) { // catch any errors KillSound(); AlertUser(theErr, sSoundErr); } } else AlertUser(ResError(), sResErr); // I’ll return the resource error } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is a demonstration of the note synth’s tone qualities (or the lack there of). This simply loops through timbres sending alternating note and rest commands. Once all the notes have been sent, then I need to send a callBackCmd to signal the SoundUnit to dispose of the channel. Well, actually the SoundUnit will set a global flag that the application will be polling for later in the event loop. Once this happens, or if any errors are encountered along the way, KillSound will be called. One very disappointing aspect to changing the timbre is that the Mac Plus or SE cannot handle this while a note is sounding. The Apple Sound Chip can and if you wanted to remove the rests try this routine on a Mac II, for example, you can hear a continuous note while the timbre changes. Try this on a Mac Plus and you’ll hear garbage. I’d show this myself, but how do I determine if the Mac has the ASC? BUG NOTE: There is problem when the final sound command is a freqDurationCmd. The note will continue to sound, looping forever, until a quietCmd is sent or the channel is disposed of. To prevent unwanted looping, I send a quietCmd after all notes. */ #pragma segment Main void PlaySquareTimbres(void) { SndChannelPtr chan; short timbre; OSErr theErr; theErr = GetSquareWaveChan(&chan, kPreferredTimbre); if (theErr == noErr) { ShowStatusWindow(sTimbresMsg); for (timbre = kSineWave; timbre <= kSquareWave; timbre += 8) { theErr = SetSquareWaveTimbre(chan, timbre, kWait); if (theErr == noErr) theErr = SendNote(chan, kOneSecond / 2, kOctave7 + Akey); if (theErr == noErr) theErr = SendRest(chan, kOneSecond / 10); timbre = timbre + 8; // skip a few more timbres if (theErr != noErr) // if there was an error... break; // get out of the loop } } if (theErr == noErr) theErr = SoundComplete(chan); else { // catch any errors KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This demonstrates the wave table synthesizers. First thing to do is obtain a wave cycle and the 'snd ' resource containing a song to be played. Then I’m ready to get the four wave channels and install the chosen wave. Once the wave is installed into the channel I can dispose of the memory used to create the wave, since the Sound Manager will copy it to its internal buffers. At this point I’m ready to play the song. It’s not easy to hear four wave synths playing the same note with the same wave table. I was going to add a modifier to the channel that would transpose each channels note, but I found a bug. BUG NOTE: Installing a modifier to one of the wave table channels caused the channel to fail. My normal sequence of events is this: I synchronize the four channels, send all the note commands, end this with the callBackCmd, and finally release the channels to play their queues. When I added a modifier, the callBackCmd was the first command to be processed. This caused my completion routines to be called and before the channel made any sound it was disposed. I tried this without the callBackCmd and the channel never processed any commands. BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound chip based Mac. At least not so far with any System 6.0x releases. This leaves the Mac Plus/SE without four tone polyphonic sound. VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager 2 and later fixed the problem. */ #pragma segment Main void PlayWaveScale(void) { SndChannelPtr chan1, chan2, chan3, chan4; SndListHandle sndSong; SndListHandle waveHandle; Ptr waveTablePtr; long offSet; short sndType; short waveLength; OSErr theErr; sndSong = (SndListHandle)Get1Resource(soundListRsrc, rScaleSnd); if (sndSong != nil) { waveHandle = (SndListHandle)Get1Resource(soundListRsrc, rTenorVox); theErr = HoldSnd(waveHandle); if (theErr == noErr) { offSet = GetSndDataOffset(waveHandle, &sndType, &waveLength); waveTablePtr = (Ptr)((long)*waveHandle + offSet); theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4); if (theErr == noErr) { theErr = InstallWave(chan1, waveTablePtr, waveLength); if (theErr == noErr) { theErr = InstallWave(chan2, waveTablePtr, waveLength); if (theErr == noErr) { theErr = InstallWave(chan3, waveTablePtr, waveLength); if (theErr == noErr) theErr = InstallWave(chan4, waveTablePtr, waveLength); } } } HUnlock((Handle)waveHandle); HPurge((Handle)waveHandle); if (theErr == noErr) theErr = Play4ChanSongs(chan1, chan2, chan3, chan4, sndSong, sndSong, sndSong, sndSong); if (theErr == noErr) ShowStatusWindow(sScaleMsg); else { // catch any SoundUnit errors KillSound(); AlertUser(theErr, sSoundErr); } } else AlertUser(theErr, sResErr); // couldn’t get waveHandle } else AlertUser(ResError(), sResErr); // couldn’t get song } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is a utility routine used to obtain the four snd resources that contain parts of a song. Nothing much to do other then get the resource and do error checking. */ #pragma segment Main OSErr GetSndSongs(short sndSongID1, short sndSongID2, short sndSongID3, short sndSongID4, SndListHandle *sndSong1, SndListHandle *sndSong2, SndListHandle *sndSong3, SndListHandle *sndSong4) { OSErr result = noErr; // initialize result // get all the snds *sndSong1 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID1); if (sndSong1 != nil) { *sndSong2 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID2); if (sndSong2 != nil) { *sndSong3 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID3); if (sndSong3 != nil) *sndSong4 = (SndListHandle)Get1Resource(soundListRsrc, sndSongID4); } } if ((sndSong1 == nil) || (sndSong2 == nil) || (sndSong3 == nil) || (sndSong4 == nil)) result = ResError(); // return resource error return (result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This routine is a utility I’m using to prepare four wave table channels. First thing to do is obtain a wave table data I stored in a set of 'snd ' resources. Then I’m ready to install these wave tables into the four wave channels. Once the waves are installed into the channel I can dispose of the memory used to create each wave, since the Sound Manager will copy them to its internal buffers. At this point the channels are ready to play sounds. */ #pragma segment Main OSErr InstallWaveSnds(SndChannelPtr chan1, SndChannelPtr chan2, SndChannelPtr chan3, SndChannelPtr chan4, short waveID1, short waveID2, short waveID3, short waveID4) { SndListHandle waveSnd1, waveSnd2, waveSnd3, waveSnd4; Ptr wavePtr; long offSet; short waveLgth; short sndType; OSErr theErr; // get em and hold em down waveSnd1 = (SndListHandle)Get1Resource(soundListRsrc, waveID1); theErr = HoldSnd(waveSnd1); if (theErr == noErr) { waveSnd2 = (SndListHandle)Get1Resource(soundListRsrc, waveID2); theErr = HoldSnd(waveSnd2); if (theErr == noErr) { waveSnd3 = (SndListHandle)Get1Resource(soundListRsrc, waveID3); theErr = HoldSnd(waveSnd3); if (theErr == noErr) { waveSnd4 = (SndListHandle)Get1Resource(soundListRsrc, waveID4); theErr = HoldSnd(waveSnd4); } } } if (theErr != noErr) return (theErr); // we’re out of here // catch the waves offSet = GetSndDataOffset(waveSnd1, &sndType, &waveLgth); wavePtr = (Ptr)((long)*waveSnd1 + offSet); theErr = InstallWave(chan1, wavePtr, waveLgth); if (theErr == noErr) { offSet = GetSndDataOffset(waveSnd2, &sndType, &waveLgth); wavePtr = (Ptr)((long)*waveSnd1 + offSet); theErr = InstallWave(chan2, wavePtr, waveLgth); if (theErr == noErr) { offSet = GetSndDataOffset(waveSnd3, &sndType, &waveLgth); wavePtr = (Ptr)((long)*waveSnd1 + offSet); theErr = InstallWave(chan3, wavePtr, waveLgth); if (theErr == noErr) { offSet = GetSndDataOffset(waveSnd4, &sndType, &waveLgth); wavePtr = (Ptr)((long)*waveSnd1 + offSet); theErr = InstallWave(chan4, wavePtr, waveLgth); } } } HUnlock((Handle)waveSnd1); // done with resources HPurge((Handle)waveSnd1); HUnlock((Handle)waveSnd2); HPurge((Handle)waveSnd2); HUnlock((Handle)waveSnd3); HPurge((Handle)waveSnd3); HUnlock((Handle)waveSnd4); HPurge((Handle)waveSnd4); return (theErr); // return the error } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is the demonstration of the wave table synthesizers. This will get four snd resources that contain parts to a song. Then I get the four wave table channels. With these four channels, I install two other snd resources that contain wave table data. Once the four songs, four channels, and two wave tables are ready then I play the song. BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound chip based Mac. At least not so far with any System 6.0x release. This leaves the Mac Plus/SE without four tone polyphonic sound. VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager 2 and later fixed the problem. */ #pragma segment Main void PlayWaveMelody(void) { SndListHandle sndSong1, sndSong2, sndSong3, sndSong4; SndChannelPtr chan1, chan2, chan3, chan4; OSErr theErr; theErr = GetSndSongs(rMelodyPart1, rMelodyPart2, rMelodyPart3, rMelodyPart4, &sndSong1, &sndSong2, &sndSong3, &sndSong4); if (theErr != noErr) { AlertUser(theErr, sResErr); // return the error return; // we’re out of here } theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4); if (theErr == noErr) { theErr = InstallWaveSnds(chan1, chan2, chan3, chan4, rWaveMelody, rWaveHarmony, rWaveHarmony, rWaveHarmony); if (theErr == noErr) theErr = Play4ChanSongs(chan1, chan2, chan3, chan4, sndSong1, sndSong2, sndSong3, sndSong4); } if (theErr == noErr) ShowStatusWindow(sMelodyMsg); else { // catch any errors KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is the demonstration of the wave table synthesizers. This will get four snd resources that contain parts to a song. Then I get the four wave table channels. With these four channels, I install four other snd resources that contain wave table data. Once the four songs, four channels, and four wave tables are ready then I play the song. By the way, SATB stands for Soprano, Alto, Tenor, and Bass. It’s standard music-speak. BUG NOTE: The wave table synthesizer is not working on a non-Apple Sound chip based Mac. At least not so far with any System 6.0x release. This leaves the Mac Plus/SE without four tone polyphonic sound. VERSION 1.2: No longer need to call HasWorkingWaveTables since Sound Manager 2 and later fixed the problem. */ #pragma segment Main void PlayWaveSATB(void) { SndListHandle sndSong1, sndSong2, sndSong3, sndSong4; SndChannelPtr chan1, chan2, chan3, chan4; OSErr theErr; theErr = GetSndSongs(rCounterPt1, rCounterPt2, rCounterPt3, rCounterPt4, &sndSong1, &sndSong2, &sndSong3, &sndSong4); if (theErr != noErr) { AlertUser(theErr, sResErr); // return the error return; // we’re out of here } theErr = GetWaveChans(&chan1, &chan2, &chan3, &chan4); if (theErr == noErr) { theErr = InstallWaveSnds(chan1, chan2, chan3, chan4, rSopranoVox, rAltoVox, rTenorVox, rBassVox); if (theErr == noErr) theErr = Play4ChanSongs(chan1, chan2, chan3, chan4, sndSong1, sndSong2, sndSong3, sndSong4); } if (theErr == noErr) ShowStatusWindow(sCounterPtMsg); else { // catch any errors KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.2: Create four sample sound channels with an instrument sound installed in each one. Then play the four part song to demonstrate the sample sound channel. */ #pragma segment Main void PlaySampleSATB(void) { SndListHandle sndSong1, sndSong2, sndSong3, sndSong4; SndListHandle sampleHarmony; SndChannelPtr chan1, chan2, chan3, chan4; OSErr theErr; theErr = GetSndSongs(rCounterPt1, rCounterPt2, rCounterPt3, rCounterPt4, &sndSong1, &sndSong2, &sndSong3, &sndSong4); if (theErr != noErr) { AlertUser(theErr, sResErr); // return the error return; // we’re out of here } sampleHarmony = (SndListHandle)Get1Resource(soundListRsrc, rSampleHarmony); theErr = Get4SampleInstruments(&chan1, &chan2, &chan3, &chan4, sampleHarmony, sampleHarmony, sampleHarmony, sampleHarmony); if (theErr == noErr) theErr = Play4ChanSongs(chan1, chan2, chan3, chan4, sndSong1, sndSong2, sndSong3, sndSong4); if (theErr == noErr) ShowStatusWindow(sCounterPtMsg); else // catch any errors { KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.2: Create four sample sound channels with an instrument sound installed in each one. Then play the four part song to demonstrate the sample sound channel. */ #pragma segment Main void PlaySampleMelody(void) { SndListHandle sndSong1, sndSong2, sndSong3, sndSong4; SndListHandle sampleHarmony, sampleMelody; SndChannelPtr chan1, chan2, chan3, chan4; OSErr theErr; theErr = GetSndSongs(rMelodyPart1, rMelodyPart2, rMelodyPart3, rMelodyPart4, &sndSong1, &sndSong2, &sndSong3, &sndSong4); if (theErr != noErr) { AlertUser(theErr, sResErr); // return the error return; // we’re out of here } sampleHarmony = (SndListHandle)Get1Resource(soundListRsrc, rSampleHarmony); sampleMelody = (SndListHandle)Get1Resource(soundListRsrc, rSampleMelody); theErr = Get4SampleInstruments(&chan1, &chan2, &chan3, &chan4, sampleMelody, sampleHarmony, sampleHarmony, sampleHarmony); if (theErr == noErr) theErr = Play4ChanSongs(chan1, chan2, chan3, chan4, sndSong1, sndSong2, sndSong3, sndSong4); if (theErr == noErr) ShowStatusWindow(sMelodyMsg); else // catch any errors { KillSound(); AlertUser(theErr, sSoundErr); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This is just one of the reasons I hate the Dialog Manager. I needed a simple dialog to ask the user for a name for their new sound. To do this, I only needed a simple dialog with two buttons and an editable text item. Since the ModalDialog will not draw an outline around the default item, I then needed an userItem. This is the really stupid part. The userItem is only there just to get a chance to draw an outline around the default button. It has no other purpose. It is not visible, not does it get any user interaction what so ever. It's just a pain in the ass. */ #pragma segment Main pascal void DefaultOutline(WindowPtr window, short theItem) { #pragma unused (theItem) Handle itemHndl; Rect itemRect; short kind; GetDialogItem((DialogPtr)window, ok, &kind, &itemHndl, &itemRect); DoButtonOutline((ControlHandle)itemHndl); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: Show a dialog asking the user to name their new sound resource. I need to outline the default button, so I have to install an userItem setting its drawing procedure to do the outline around a different item altogether. The Dialog Manager really bugs me. */ #pragma segment Main void GetSndName(Str255 sndName) { DialogPtr dialog; Handle itemHndl; Rect itemRect; short kind; short theItem; dialog = GetNewDialog(rGetNameDLOG, nil, (WindowPtr)-1); GetDialogItem(dialog, rUserItem, &kind, &itemHndl, &itemRect); SetDialogItem(dialog, rUserItem, kind, (Handle)DefaultOutline, &itemRect); do ModalDialog(nil, &theItem); while ((theItem != ok) && (theItem != cancel)); if (theItem == ok) { GetDialogItem(dialog, rNameItem, &kind, &itemHndl, &itemRect); GetDialogItemText(itemHndl, sndName); } else sndName[0] = 0; DisposeDialog(dialog); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine adds the given snd resource to the file and document. First thing to do is find a proper resource ID for the 'snd '. There is a reserved range for them, didn't you read the documentation? Once this new resource is added then I update the file and re-build the list of sounds. This is necessary since the list must be in the same order as the sounds are in the file. I ignore the error returned by InitSndList, since the chances of it failing at this point are slim. If anything is wrong with the list, then the rest of this application is robust enough to handle a resource problem. The user would have to close the document and attempt to open it again. If adding the resources works, but updating the file fails then I will remove it. This keeps the document consistent with the file. */ #pragma segment Main OSErr AddSnd(SndDocPeek sndDoc, StringPtr sndNamePtr, SndListHandle sndHndl) { short resID; OSErr theErr; short ignore; UseResFile(sndDoc->resFile); //put our resource in the right file do resID = Unique1ID(soundListRsrc); while (resID < kSystemSndRange); AddResource((Handle)sndHndl, soundListRsrc, resID, sndNamePtr); theErr = ResError(); UseResFile(gAppResRef); //restore our resource file if (theErr == noErr) { UpdateResFile(sndDoc->resFile); //update the file theErr = ResError(); if (theErr == noErr) { ignore = FlushVol(nil, sndDoc->vRefNum); LDelRow(0, 0, sndDoc->list); //delete all the rows ignore = InitSndList(sndDoc); //re-create the list SelectSndCell(sndDoc, resID); //select the new snd } else RemoveResource((Handle)sndHndl); //couldn't add it, update failed } return(theErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine has one tricky aspect involving the Resource Manager and removing a resource. There's a catch. I want the document to always reflect the contents of the file on disk. In other words, InitSndList depends on UpdateResFile returning noErr. If the file is locked, then removing a resource shouldn't be allowed but RmveResource will return noErr even if the file cannot be updated. This is a "feature" since the user may unlock the volume after removing the resource and then UpdateResFile would work. If RmveResource succeeds, but UpdateResFile fails then the resource map in memory will be inconsistent with what's on disk. If the user then closed the document and opened it again, the resource they just removed would magically still be there. The basic problem is that it is difficult to determine if a given file will really allow write access. There's at least three situations to check: a locked file, a locked volume, or an AppleShare folder privileges issue. The ideal, and probably the best, solution would be to deselect the edit commands from the user for a read-only file. This would involve lots of File Manager calls every time the user selects a menu command. (If you thought that GetFCBInfo would tell you this, you're wrong. You can have write permission to a file that is on a locked volume.) Additionally, the user needs to see that the reason the edit menu doesn't work is because the file is read-only which would require another feature to be added with lots more code. This is why I call ChangedResource before any other Resource Manager calls. ChangedResource will determine if the resource can be written to disk. If not, then it returns an error which is exactly what I wanted to know in the first place. I want to flush the cache to keep the disk's resource map consistent with the resource data. Otherwise, a crash could occur the resource fork might be damaged and the file has to be repaired (if possible) or deleted. I'm ignoring the InitSndList result. At this stage, there's only a slim chance the list couldn't be re-built. If it does fail, the file should be closed. A tip to the reader: dispose of as much memory as possible before calling UpdateResFile. This makes it faster. UpdateResFile will not purge any memory, but only attempts to use the available space. If there's little free space then UpdateResFile will run really slow. */ #pragma segment Main void ClearSnd(SndDocPeek sndDoc) { SndListHandle sndHndl; OSErr theErr; short ignore; theErr = GetSelection(sndDoc, &sndHndl); //get the resource to remove if (theErr == noErr) { ChangedResource((Handle)sndHndl); //can we change the file? theErr = ResError(); //save the error result if (theErr == noErr) { UseResFile(sndDoc->resFile); //use the right resource file RemoveResource((Handle)sndHndl); //remove that sucker theErr = ResError(); //save the error result UseResFile(gAppResRef); //restore our resource file if (theErr == noErr) { DisposeHandle((Handle)sndHndl); //get rid of the memory UpdateResFile(sndDoc->resFile); //update the file theErr = ResError(); //save the error result if (theErr == noErr) ignore = FlushVol(nil, sndDoc->vRefNum); LDelRow(0, 0, sndDoc->list); //delete all the rows ignore = InitSndList(sndDoc); //re-create the list SelectNextCell(sndDoc->list, true); //select the first cell ActivateSndCntls(sndDoc); //may not be any sounds left } } } if (theErr != noErr) AlertUser(theErr, sEditErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine will create a handle and record sound into it. A new sound will prompt the user for a name to add it to the document. The Sound Input Manager will re-size this handle to be as small as possible to contain only the samples recorded. You can pass sndHandl == nil to SndRecord, which will cause the Sound Input Manager to create a handle for you. If adding the new sound is successful, the new resource handle is marked purgeable, because I only use them temporally and they tend to be large. I have to set the sndHandle variable to nil in this case since it is now belongs to the Resource Manager and I don't want to dispose of it in the error handling code. The choice of recording quality wasn't given to the user. Typically, users would only be confused by the question of "what compression ratio do you prefer?" As a power user option, it would be nice to let the user set the rate. Be careful since this is a user interface issue, and Apple expects to see lots of Sound Input features in applications. */ #pragma segment Main void DoRecordSound(SndDocPeek sndDoc) { Str255 sndName; Point recTopLeft; long total; long contig; SndListHandle sndHandle; OSErr theErr; KillSound(); PurgeSpace(&total, &contig); sndHandle = (SndListHandle)NewHandle(contig - kMinSpace); if (sndHandle != nil) { recTopLeft.v = kRecordTop; recTopLeft.h = kRecordLeft; theErr = SndRecord(nil, recTopLeft, siBestQuality, &sndHandle); if (theErr == noErr) { GetSndName(sndName); theErr = AddSnd(sndDoc, sndName, sndHandle); if (theErr == noErr) { HPurge((Handle)sndHandle); sndHandle = nil; } } } else theErr = MemError(); if (sndHandle != nil) DisposeHandle((Handle)sndHandle); if ((theErr != noErr) && (theErr != userCanceledErr)) AlertUser(theErr, sSoundErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Enable and disable menus based on the current state. The user can only select enabled menu items so I set up all the menu items before calling MenuSelect or MenuKey, since these are the only times that a menu item can be selected. Note that MenuSelect is also the only time the user will see menu items. This approach to deciding what enable/disable state a menu item has the advantage of concentrating all the decision making in one routine, as opposed to being spread throughout the application. Other application designs may take a different approach that may or may not be just as valid. */ #pragma segment Main void AdjustMenus(void) { MenuHandle menu; WindowPtr window; Boolean allowEdit; window = FrontWindow(); menu = GetMenuHandle(mFile); //the File menu and items if (IsDAWindow(window) || IsDocWindow(window)) EnableItem(menu, iClose); else DisableItem(menu, iClose); menu = GetMenuHandle(mEdit); //the Edit menu and items allowEdit = IsDAWindow(window); if (allowEdit) //check the undo item EnableItem(menu, iUndo); else DisableItem(menu, iUndo); if (IsDocWindow(window)) { //handle the other edit items allowEdit = HasSelection((SndDocPeek)window) || allowEdit; EnableItem(menu, iPaste); } else DisableItem(menu, iPaste); if (allowEdit) { EnableItem(menu, iCut); EnableItem(menu, iCopy); EnableItem(menu, iClear); } else { DisableItem(menu, iCut); DisableItem(menu, iCopy); DisableItem(menu, iClear); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: Get the currently selected item and copy it to the clipboard. I want to also save the name for this resource, so I add a string type to the clipboard as well. */ #pragma segment Main void CopySnd(SndDocPeek sndDoc) { Str255 sndName; ResType rType; SndListHandle sndHndl; long scrapLen; short id; OSErr theErr; theErr = GetSelection(sndDoc, &sndHndl); if (theErr == noErr) { GetResInfo((Handle)sndHndl, &id, &rType, sndName); scrapLen = ZeroScrap(); //ignoring the result theErr = PutScrap(StrLength(sndName) + 1, 'STR ', sndName); HLock((Handle)sndHndl); theErr = PutScrap(GetHandleSize((Handle)sndHndl), soundListRsrc, (Ptr)(*sndHndl)); HUnlock((Handle)sndHndl); HPurge((Handle)sndHndl); } if (theErr != noErr) AlertUser(theErr, sEditErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: Copy the selection and then delete it. */ #pragma segment Main void CutSnd(SndDocPeek sndDoc) { CopySnd(sndDoc); ClearSnd(sndDoc); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* VERSION 1.1: This routine will create a new resource into the file containing the clipboard's sound data. A name is attempted to be found by first checking for the string type in the scrap. This application will always put both a string and a sound together on the clipboard. But if the user copied a sound from the Sound cdev, ResEdit, or by some other method then I'll ask the user for a new name. If adding the new resource is successful, then I mark the resource as purgeable and set the sndHndl variable to NIL. The Resource Manager then owns the handle and I don't want my error handling to dispose of this new handle. */ #pragma segment Main void PasteSnd(SndDocPeek sndDoc) { Str255 sndName; StringHandle sndNameHndl; long offset; long scrapLen; SndListHandle sndHndl; OSErr theErr; sndHndl = (SndListHandle)NewHandle(0); sndNameHndl = NewString("\p"); theErr = MemError(); scrapLen = GetScrap((Handle)sndNameHndl, 'STR ', &offset); scrapLen = GetScrap((Handle)sndHndl, soundListRsrc, &offset); if (scrapLen > 0) { if (sndNameHndl != nil) PStringCopy(*sndNameHndl, sndName); else sndName[0] = 0; if (StrLength(sndName) == 0) GetSndName(sndName); theErr = AddSnd(sndDoc, sndName, sndHndl); if (theErr == noErr) { HPurge((Handle)sndHndl); sndHndl = nil; //done with handle ActivateSndCntls(sndDoc); } } else theErr = scrapLen; if (sndHndl != nil) DisposeHandle((Handle)sndHndl); if (sndNameHndl != nil) DisposeHandle((Handle)sndNameHndl); if (theErr != noErr) AlertUser(theErr, sEditErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called when an item is chosen from the menu bar (after calling MenuSelect or MenuKey). It performs the right operation for each command. It is good to have both the result of MenuSelect and MenuKey go to one routine like this to keep everything organized. VERSION 1.1: Supporting the edit commands for snd resources. */ #pragma segment Main void DoMenuCommand(long menuResult) { WindowPtr window; short menuID; short menuItem; //resource ID and item of the selected menu short daRefNum; Str255 daName; Boolean ignore; menuID = HighWord(menuResult); //use macros for efficiency... menuItem = LowWord(menuResult); //to get menu item number and menu number switch (menuID) { case mApple: switch (menuItem) { case iAbout: //bring up alert for About DoAbout(); break; default: //all non-About items in this menu are DAs GetMenuItemText(GetMenuHandle(mApple), menuItem, daName); daRefNum = OpenDeskAcc(daName); break; } break; case mFile: switch (menuItem) { case iNew: NewSoundDoc(); break; case iOpen: GetSoundDoc(); break; case iClose: ignore = DoCloseWindow(FrontWindow()); //I don’t care if cancelled break; case iQuit: Terminate(); break; } break; case mEdit: //call SystemEdit for DA editing && MultiFinder if (! SystemEdit(menuItem - 1)) { //since I don’t do any editing window = FrontWindow(); if (IsDocWindow(window)) { switch (menuItem) { case iCut: CutSnd((SndDocPeek)window); break; case iCopy: CopySnd((SndDocPeek)window); break; case iPaste: PasteSnd((SndDocPeek)window); break; case iClear: ClearSnd((SndDocPeek)window); break; } } } break; case mDemos: switch (menuItem) { case iCheckVolume: CheckSoundVolume(); break; case iSquareScale: PlaySquareSong(rScaleSnd); break; case iSquareMelody: PlaySquareSong(rMelodyPart1); break; case iSquareTimbre: PlaySquareTimbres(); break; case iWaveScale: PlayWaveScale(); break; case iWaveMelody: PlayWaveMelody(); break; case iWaveSATB: PlayWaveSATB(); break; case iSampleMelody: PlaySampleMelody(); break; case iSampleSATB: PlaySampleSATB(); break; } //switch (menuItem) } //switch (menuID) HiliteMenu(0); //unhighlight what MenuSelect or MenuKey hilited } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Draw the contents of the application window in response to an update event. At this point, BeginUpdate has been called which sets the window’s visRgn to clip drawing only where it needs to be done. I have the controls to draw, a list to update, and a default button to outline. I use UpdateControls to avoid needless drawing that happens with DrawControls. It not only runs faster but doesn’t flicker. */ #pragma segment Main void DrawSndWindow(WindowPtr window) { ControlHandle control; Rect theRect; PenNormal(); LUpdate(window->visRgn, ((SndDocPeek)window)->list); // update list theRect = (**((SndDocPeek)window)->list).rView; // frame the list theRect.right = theRect.right + kScrollbarAdjust; InsetRect(&theRect, kListFrameInset, kListFrameInset); FrameRect(&theRect); UpdateControls(window, window->visRgn); // update controls control = ((WindowPeek)window)->controlList; // draw button outline while (control != nil) { if (GetControlReference(control) == rPlaySndCntl) DoButtonOutline(control); control = (**control).nextControl; } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* When the user types a key, I check if it is one that I’m looking for. This routine only handles the non-command key events. Here I’m looking for a arrow key or the return and enter keys for the default button. VERSION 1.1: The enter and return keys only work if there is a selection. Now supporting the backspace and delete keys which clear the selection. */ #pragma segment Main void DoKeyDown(char key, WindowPtr window) { ControlHandle control; Boolean ignore; switch (GetWRefCon(window)) { case rSoundWindow: switch (key) { case kEnterKey: case kReturnKey: if (HasSelection((SndDocPeek)window)) { control = ((WindowPeek)window)->controlList; while (control != nil) { //find the default button if (GetControlReference(control) == rPlaySndCntl) SelectButton(control); //here it is control = (**control).nextControl; } PlaySelectedSnd((SndDocPeek)window, sPlayingMsg); } break; case kUpArrow: SelectNextCell(((SndDocPeek)window)->list, false); ActivateSndCntls((SndDocPeek)window); break; case kDownArrow: SelectNextCell(((SndDocPeek)window)->list, true); ActivateSndCntls((SndDocPeek)window); break; case kBackspace: ClearSnd((SndDocPeek)window); break; } break; //rSoundWindow case rStatusWindow: if ( (key == kEnterKey) || (key == kReturnKey) || (key == kEscape) ) { SelectButton(((WindowPeek)window)->controlList); KillSound(); } break; case rAboutWindow: if ( (key == kEnterKey) || (key == kReturnKey) || (key == kEscape) ) { SelectButton(((WindowPeek)window)->controlList); ignore = DoCloseWindow(window); } break; } //switch (GetWRefCon(window)) } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Given a document, this will handle any clicking in the list. The mouse point (event.where) is expected to have been adjusted to the local coordinates of the window. If this routine does handle the event, then I return true. BUG NOTE: LClick will return a double-click even if no cell was selected. So I have to test for the last click being in a real cell. If not, then I will not process the double click. Also, there is another bug in the List Manager. It will not always deselect the currently selected cell when the user clicks outside of the data bounds. In other words, sometimes my list would show 4 items when the list has room to show 8. If the user clicked in the bottom area of the list (below the last item) the List Manager should deselect any items. It doesn’t all the time, just sometimes. The only real solution would be to write a new LClick. */ #pragma segment Main Boolean ListClick(SndDocPeek sndDoc, EventRecord *event) { Cell aCell; Rect listRect; Boolean result; listRect = (**(sndDoc->list)).rView; listRect.right = listRect.right + kScrollbarAdjust; if (PtInRect(event->where, &listRect)) { if (LClick(event->where, event->modifiers, sndDoc->list)) { aCell = LLastClick(sndDoc->list); if ( PtInRect(aCell, &((**(sndDoc->list)).dataBounds)) ) PlaySelectedSnd(sndDoc, sPlayingMsg); } ActivateSndCntls(sndDoc); result = true; //I handled the event } else result = false; return(result); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called when a mouse-down event occurs in the content of my application windows. First thing to check for is a click in the list. If not a list click, then find what control may have been clicked in and handle that. VERSION 1.1: Handle the record button. */ #pragma segment Main void DoSndDocClick(SndDocPeek sndDoc, EventRecord *event) { ControlHandle control; SetPort((GrafPtr)sndDoc); GlobalToLocal(&event->where); if (! ListClick(sndDoc, event)) { if (FindControl(event->where, (WindowPtr)sndDoc, &control) != 0) { if (TrackControl(control, event->where, nil) != 0) { switch (GetControlReference(control)) { case rPlaySndCntl: PlaySelectedSnd(sndDoc, sPlayingMsg); break; case rHyperPlayCntl: PlaySelectedSnd(sndDoc, sHyperMsg); break; case rPlayScaleCntl: PlaySndSong(sndDoc, rScaleSnd); break; case rMelodyCntl: PlaySndSong(sndDoc, rMelodyPart1); break; case rStopCntl: KillSound(); break; case rRecordCntl: DoRecordSound(sndDoc); break; } //switch GetControlReference(control) } //if TrackControl } //if FindControl } //if ! ListSelect } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* The status window has a stop button. This will stop any sound in progress. So, if the user clicks in my status window I need to check for this. */ #pragma segment Main void DoStatClick(StatWindowPeek statWindow, EventRecord *event) { ControlHandle control; SetPort((GrafPtr)statWindow); GlobalToLocal(&(event->where)); if (FindControl(event->where, (WindowPtr)statWindow, &control) != 0) if (TrackControl(control, event->where, nil) != 0) KillSound(); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* The about window has a single OK button. If the users clicks in it, then close the window. */ #pragma segment Main void DoAboutClick(WindowPtr window, EventRecord *event) { ControlHandle control; Boolean ignore; SetPort(window); GlobalToLocal(&event->where); if (FindControl(event->where, (WindowPtr)window, &control) != 0) { if (TrackControl(control, event->where, nil) != 0) ignore = DoCloseWindow(window); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called when an update event is received for any of my windows. It calls the appropriate window’s update routine to draw its contents. As an efficiency measure that does not have to be followed, it calls the drawing routine only if the visRgn is non-empty. This will handle situations where calculations for drawing or drawing itself is very time-consuming. Why does QD give you an update with an empty updateRgn? */ #pragma segment Main void DoUpdate(WindowPtr window) { BeginUpdate(window); //setup the visRgn, clears updateRgn if (! EmptyRgn(window->visRgn)) { //if updating to be done SetPort(window); //set to the current port switch (GetWRefCon(window)) { //call the window’s drawing routine case rSoundWindow: DrawSndWindow(window); break; case rStatusWindow: DrawStatusWindow(); break; case rAboutWindow: DrawAboutWindow(window); break; } } EndUpdate(window); //restores the visRgn } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This is called when a window is to be activated or deactivated. For the document window I activate all the controls and the list. For the status window I activate the one and only control. This is also called for suspend and resume events while running MultiFinder. */ #pragma segment Main void DoActivate(WindowPtr window, Boolean becomingActive) { if (window != nil) { switch (GetWRefCon(window)) { case rSoundWindow: ActivateSndCntls((SndDocPeek)window); LActivate(becomingActive, ((SndDocPeek)window)->list); break; case rStatusWindow: if (becomingActive) //it only has one control HiliteControl(((WindowPeek)window)->controlList, kControlNoPart); else HiliteControl(((WindowPeek)window)->controlList, kControlInactivePart); DoButtonOutline(((WindowPeek)window)->controlList); break; case rAboutWindow: if (becomingActive) //it only has one control HiliteControl(((WindowPeek)window)->controlList, kControlNoPart); else HiliteControl(((WindowPeek)window)->controlList, kControlInactivePart); DoButtonOutline(((WindowPeek)window)->controlList); break; } } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* The required AppleEvent sent to tell an application to quit. Nothing much to do. Just call our terminate routine to clean up and then exit through the event loop. Note that you cannot exit from here, the system will crash. You must return to the AppleEvent Manager. Do not call ExitToShell at this point! */ #pragma segment Main pascal OSErr QuitApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon) { #pragma unused (theAppleEvent, reply, handlerRefcon) Terminate(); return(noErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* One of the required AppleEvents. This is a request to open a document. Find the document being requested, and open it. */ #pragma segment Main pascal OSErr OpenDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon) { #pragma unused (reply, handlerRefcon) FSSpec file; AEDescList docList; long i; long itemsInList; Size actualSize; AEKeyword keyword; DescType returnedType; OSErr theErr; // get the direct parameter--a descriptor list--and put it into docList theErr = AEGetParamDesc(theAppleEvent, keyDirectObject, typeAEList, &docList); if (theErr == noErr) { if (docList.descriptorType != typeAEList) { AEDisposeDesc (&docList); theErr = paramErr; } else { // count the number of descriptor records in the list theErr = AECountItems(&docList, &itemsInList); if (theErr == noErr) { //get each descriptor record from the list, coerce the returned data //to an FSSpec and open the associated file for (i = 1; i <= itemsInList; i++) { theErr = AEGetNthPtr(&docList, i, typeFSS, &keyword, &returnedType, &file, sizeof(file), &actualSize); if (theErr == noErr) OpenSoundDoc(&file); } } AEDisposeDesc (&docList); } } return (theErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* It's a required AppleEvent and we don't use it. */ #pragma segment Main pascal OSErr PrintDocumentsEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon) { #pragma unused (theAppleEvent, reply, handlerRefcon) return (errAEEventNotHandled); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* It's a required AppleEvent and we don't use it. */ #pragma segment Main pascal OSErr OpenApplicationEvent(const AppleEvent *theAppleEvent, AppleEvent *reply, long handlerRefcon) { #pragma unused (theAppleEvent, reply, handlerRefcon) return (noErr); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* As Spike Lee says, “Do the right thing.” Determine what kind of event it is, and call the appropriate routines. Key down events are first tested as being command key events for menus or command-. for cancel. If it’s the command-. the user is asking to cancel the sound. In this case, if the front window is the status window then the button should look as if it has been clicked. This is proper human interface. Any non-menu command keys are passed to DoKeyDown. I have a global flag, gInModalState, which is used to handle the front window as a modal dialog. This means I ignore clicks outside of the modal window and menu command keys. I could have a different global flag that would mean it’s a modal window but uses menu commands, such as gInModalMenuState. An important note is that I do pass any other non-menu command keys to DoKeys, so the modal window could have access to key events. One last thing to note happens during a suspend event. Applications using sound should dispose of their sound channels at suspend time. No other application can use sound while another one has channels allocated. To stop my sound, I call KillSound which hides the status window. MultiFinder does properly deactivate the front window at suspend time and applications normally do not have to worry about HiliteWindow. I do because the front window could have been the status window and I’ve removed it after getting a suspend event. This causes the next window, a document window, to become highlighted as the active window. To avoid my next window from being highlighted while in the background, I have to call HiliteWindow *after* KillSound. This is strange and only applications changing the front window at suspend event time have to be concerned about this. VERSION 1.1: Added a constant, kSFTopLeft, to specify the dialog position. I do not call KillSound during a MultiFinder switch unless we're running the older Sound Manager. The new one allows for multiple sound channels or will return the proper error otherwise. */ #pragma segment Main void DoEvent(EventRecord *event) { WindowPtr window; Point where; short part; short err; Boolean ignore; char key; switch (event->what) { case mouseDown: part = FindWindow(event->where, &window); if ( (IsModalWindow(FrontWindow()) && (window != FrontWindow())) || (IsModalWindow(FrontWindow()) && (part == inMenuBar))) { SysBeep(30); //click outside of modal window return; //break out of routine } switch (part) { case inMenuBar: //process the menu command AdjustMenus(); DoMenuCommand(MenuSelect(event->where)); break; case inSysWindow: //let system handle the mouseDown SystemClick(event, window); break; case inContent: if ((window != FrontWindow())) SelectWindow(window); else { switch (GetWRefCon(window)) { case rSoundWindow: DoSndDocClick((SndDocPeek)window, event); break; case rStatusWindow: DoStatClick((StatWindowPeek)window, event); break; case rAboutWindow: DoAboutClick(window, event); break; } //switch (GetWRefCon(window)) } break; //inContent case inDrag: //pass screenBits.bounds to get all gDevices DragWindow(window, event->where, &qd.screenBits.bounds); break; case inGoAway: if (TrackGoAway(window, event->where)) ignore = DoCloseWindow(window); break; } //switch (part) break; //mouseDown case keyDown: case autoKey: window = FrontWindow(); key = event->message & charCodeMask; if ((event->modifiers & cmdKey) != 0) //Command key down? { if (key == kPeriod) { if (window == (WindowPtr)gStatusWindow) SelectButton(((WindowPeek)gStatusWindow)->controlList); KillSound(); } else if ((event->what == keyDown) && (! IsModalWindow(window))) { AdjustMenus(); //adjust items properly DoMenuCommand(MenuKey(key)); } } else //non-Command keys if (window != nil) //if there’s a window DoKeyDown(key, window); break; case activateEvt: //true for activate, false for deactivate DoActivate((WindowPtr)event->message, event->modifiers & activeFlag); break; case updateEvt: //call DoUpdate with the window to update DoUpdate((WindowPtr)event->message); break; case diskEvt: //Call DIBadMount in response to a diskEvt if (HighWord(event->message) != noErr) { where.v = kRecordTop; where.h = kRecordLeft; err = DIBadMount(where, event->message); } break; case osEvt: switch (event->message >> 24) { //get high byte of message case suspendResumeMessage: if ((event->message & suspendResumeMessage) != 0) gInBackground = false; //it was a resume event else { gInBackground = true; //it was a suspend event if (GetSoundMgrVersion() == 1) KillSound(); //stop any sound window = FrontWindow(); //get front window if (window != nil) //don’t use a nil window HiliteWindow(window, false); //then properly activate it } DoActivate(FrontWindow(), ! gInBackground); break; //suspendResumeMessage } break; //osEvt case kHighLevelEvent: AEProcessAppleEvent(event); break; } //switch (event->what) } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Change the cursor’s shape, depending on its current position. This also calculates the region where the current cursor resides (for WaitNextEvent). This is based on its current position in global coordinates. If the mouse is ever outside of that region, an event is generated causing this routine to be called again by the event loop. This allows me to change the region to where the mouse is currently located. If there is more to the event than just “the mouse moved,” this gets called before the event is processed to make sure the cursor is the right one. In any (ahem) event, this is called again before I fall back into WaitNextEvent. Extreme short values are used to create a wide open region. -SHRT_MAX - 1 is the largest negative short (-32768) and SHRT_MAX is the largest positive short (32767). BUG NOTE: The largest positive value for a region’s size is SHRT_MAX - 1 due to a very old bug that still remains to this day. */ #pragma segment Main void AdjustCursor(RgnHandle region) { WindowPtr window; RgnHandle arrowRgn; RgnHandle sndCursorRgn; Rect sndCursorRect; window = FrontWindow(); //I only adjust the cursor when I am in front if ((! gInBackground) && (! IsDAWindow(window))) { arrowRgn = NewRgn(); //calculate regions for different cursor shapes sndCursorRgn = NewRgn(); //start with a big, big rectangular region SetRectRgn(arrowRgn, -SHRT_MAX - 1, -SHRT_MAX - 1, SHRT_MAX - 1, SHRT_MAX - 1); if (IsDocWindow(window)) { //calculate region for document cursor sndCursorRect = window->portRect; SetPort(window); //make a global version of the viewRect LocalToGlobal(&TopLeft(sndCursorRect)); LocalToGlobal(&BottomRight(sndCursorRect)); RectRgn(sndCursorRgn, &sndCursorRect); SetOrigin(-window->portBits.bounds.left, -window->portBits.bounds.top); SectRgn(sndCursorRgn, window->visRgn, sndCursorRgn); SetOrigin(0, 0); } DiffRgn(arrowRgn, sndCursorRgn, arrowRgn); //subtract other region if (PtInRgn(GetGlobalMouse(), sndCursorRgn)) { SetCursor(*(GetCursor(rSndCursor))); //change cursor and region CopyRgn(sndCursorRgn, region); } else { SetCursor(&qd.arrow); CopyRgn(arrowRgn, region); } DisposeRgn(arrowRgn); //get rid of our local regions DisposeRgn(sndCursorRgn); } } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Get events forever, and handle them by calling DoEvent. Since this application requires System 6.0x or later I know that _WaitNextEvent is always there, even without MultiFinder. MultiFinder’s sleep is used to determine how often I want to receive events, regardless if an event has actually occurred. In this application, I don’t perform any background processing so I’m being super friendly by using LONG_MAX as a sleep value. This also helps keep Virtual Memory from paging me in just to run my event loop and find out that nothing happened. The application will only be called upon for events that must be handled. Also call AdjustCursor each time through the loop. AdjustCursor will return the current region containing the mouse and is passed to WaitNextEvent. If the mouse travels out of the region, another event is generated. I have to call AdjustCursor just before doing the event to make sure the right cursor is shown. Another thing, if I have a sound playing asynchronously then I want to dispose of my channel as soon as possible. I set up a flag in the SoundUnit that will keep track of when it has a channel allocated. I can call the HasChannelOpen() function to find out if this is true. It pretty much like keeping track of when you’re in the background. If a channel is open, then the MultiFinder sleep time is adjusted to a reasonable time that will allow me to catch when the sound has completed so that I may dispose of my channels and status window. That’s when I return kPollingSleepTime. VERSION 1.1: No longer putting the SANELib into a seperate segment, which then needed to be unloaded. Instead, I merge it into the Main segment. Refer to the Make file for further information. VERSION 1.2: No longer using the SANELib at all. All of the SANE calls are inline, and do not need a library to be linked in. */ #pragma segment Main void EventLoop(void) { RgnHandle cursorRgn; EventRecord event; long sleep; cursorRgn = NewRgn(); //1st time pass WNE an empty region while (!gTerminate) { if (HasChannelOpen()) //if we’re playing a sound sleep = kPollingSleepTime; //use the polling sleep value else { sleep = LONG_MAX; //default value for sleep UnloadSeg(_SoundUnit); //unload the Sound Unit } UnloadSeg(OpenSoundDoc); //unload the open code if (HasSoundCompleted()) KillSound(); if (LowOnReserve()) RecoverReserve(); AdjustCursor(cursorRgn); //get the right cursor if (WaitNextEvent(everyEvent, &event, sleep, cursorRgn)) { AdjustCursor(cursorRgn); //get the right cursor DoEvent(&event); } }; //loop forever; I quit through Terminate } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* Set up the whole world. Initialize global variables, Toolbox managers, and menus. If a failure occurs here, I will consider that the application is in such bad shape that I should just exit. Your error handling may differ, but the checks should still be made. I ask for a set of master pointer blocks and all permanent storage at this point to cut down on memory fragmentation. I may be opening large numbers of resources and documents so I allocate some extra master pointer blocks at the start. VERSION 1.1: Now supports opening of documents from the Finder. */ #pragma segment Initialize void Initialize(void) { #define kBroughtToFront 3 EventRecord event; Handle menuBar; long response; short count; OSErr ignoreErr; Boolean ignoreResult; gTerminate = false; gInBackground = false; //we’ll be in the foreground soon gAppResRef = CurResFile(); //save the resRef to myself for (count = 1; count <= kNumberOfMasters; count++) //allocate master pointer blocks MoreMasters(); InitGraf(&qd.thePort); //init managers, yawn... InitFonts(); InitWindows(); InitMenus(); TEInit(); InitDialogs(nil); InitCursor(); /* ErrorSound is used to prevent the Dialog Manager from calling _SysBeep. If I get a memory or resource Manager error it wouldn’t be the best of plans to call _SysBeep which will want to allocate memory and load a few resources. Which could be bad if I just gave an Alert signalling an out of memory condition. You only have to install this for Sound Manager version 1 which is pretty buggy. VERSION 1.2: Since we're using Sound Manager 2 or later, we don't have to disable the multiple sampled sound channels demo and we don't need DoErrorSound. if (GetSoundMgrVersion() == 1) { ErrorSound(GetRoutineAddress(DoErrorSound)); DisableItem(GetMenu(mDemos), iSampleSATB); DisableItem(GetMenu(mDemos), iSampleMelody); } */ /* This code is necessary to pull the application into the foreground. I use EventAvail because I don’t want to remove any events the user may have done, such as typing ahead. Until the application has made a few calls (3 seems to be the magic number) to the Event Manager, MultiFinder keeps me in the background. Splashscreens and Alerts will remain in a background layer until we get a few events. This is documented in Tech Note #180. */ for (count = 1; count <= kBroughtToFront; count++) ignoreResult = EventAvail(everyEvent, &event); /* Ignore the error returned from SysEnvirons; even if an error occurred, the SysEnvirons glue will fill in the SysEnvRec. You can save a redundant call to SysEnvirons by calling it after initializing AppleTalk. VERSION 1.2: Using Gestalt and checking for System 7. Make sure you know if the Gestalt trap is available before calling it. This code is compiled with the SystemSevenOrLater flag on, when means you do not get the safe glue for Gestalt. */ if (! TrapExists(_Gestalt)) EmergencyExit(sWrongVersion); if (Gestalt(gestaltSystemVersion, &response) != noErr) EmergencyExit(sStandardErr); if (response < 0x0700) EmergencyExit(sWrongVersion); /* Call the SoundUnit to initialize itslef. If the SoundUnit encounters an error, then it cannot be used and this means I’m leaving too. */ if (InitSoundUnit() != noErr) //allocates 4 * 1064 bytes EmergencyExit(sInitSoundErr); /* Before I go any further, I want my reserve memory. This is an emergency reserve (sorta like my old VW had) when memory runs low. If I cannot obtain this reserve, then I’ll bail. It’s also important to obtain my reserve before testing if I have the desired amount of memory to run this application. Also, FailLowMemory will consider the memory reserve. */ if (! AllocateReserve()) EmergencyExit(sLowMemory); SetGrowZone(GetRoutineAddress(MyGrowZone)); menuBar = GetNewMBar(rMenuBar); //read menus into menu bar if (menuBar == nil) EmergencyExit(sNoMenus); //wow, how’d that happen? SetMenuBar(menuBar); //install menus DisposeHandle(menuBar); AppendResMenu(GetMenuHandle(mApple), 'DRVR'); //add DA names to Apple menu DrawMenuBar(); InitStatusWindow(); //get the status window ready InitCursorCtl(nil); //MPW’s handy cursor routines /* Last, I want to make sure that enough memory is free for my application to run. It is possible that user may have adjusted the SIZE resource to too small a setting or for some other reason the application started up in a very small memory partition. It’s also possible for a situation to arise where the heap may have been of the requested size taken from the SIZE resource, but a large scrap was loaded which left too little memory. I want to make sure that my free memory is not being modified by the scrap’s presence. So, I unload it to disk but if the application will run once the scrap is unloaded, then you’ll probably not get it back into memory. Thus losing the clipboard contents. I preform this check after initializing all the Toolbox and the basic features of this application, such as showing the about box. */ if (FailLowMemory(kMinSpace)) { if (UnloadScrap() != noErr) EmergencyExit(sLowMemory); else { if (FailLowMemory(kMinSpace)) EmergencyExit(sLowMemory); } } /* Install the four core AppleEvent handlers. */ ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEQuitApplication, GetRoutineAddress(QuitApplicationEvent), 0, false); ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEOpenDocuments, GetRoutineAddress(OpenDocumentsEvent), 0, false); ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEPrintDocuments, GetRoutineAddress(PrintDocumentsEvent), 0, false); ignoreErr = AEInstallEventHandler(kCoreEventClass, kAEOpenApplication, GetRoutineAddress(OpenApplicationEvent), 0, false); } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* This routine is contained in the MPW runtime library. It will be placed into the code segment used to initialize the A5 globals. This external reference to it is done so that we can unload that segment, named %A5Init. */ #ifdef applec extern void _DataInit(void); #endif //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /* If you have stack requirements that differ from the default, then you could use SetApplLimit to increase StackSpace at this point, before calling MaxApplZone. */ #pragma segment Main void main(void) { #ifdef applec UnloadSeg(_DataInit); //note that _DataInit must not be in Main! #endif MaxApplZone(); //expand the heap so code segments load at the top Initialize(); //initialize the program UnloadSeg(Initialize); //note that Initialize must not be in Main! EventLoop(); //call the main event loop ExitToShell(); //we're out of here! } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MAIN PROGRAM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~